From dafad423b4a939364c4e460b4ff257123876222b Mon Sep 17 00:00:00 2001 From: TheSaifZaman Date: Tue, 28 Jul 2026 16:05:06 +0600 Subject: [PATCH 1/6] build: add @aws-sdk/client-iam to the api package Both lockfiles updated with the manifest: CI installs the workspace with pnpm install --frozen-lockfile and the api package with bun install --frozen-lockfile, so updating only one fails a job. --- bun.lock | 59 ++++++++- packages/api/package.json | 1 + pnpm-lock.yaml | 263 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 322 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index 76e7b48..c925a7b 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "dependencies": { "@aws-sdk/client-ec2": "^3.1076.0", "@aws-sdk/client-eks": "^3.1076.0", + "@aws-sdk/client-iam": "^3.1096.0", "@aws-sdk/client-lambda": "^3.1076.0", "@aws-sdk/client-rds": "^3.1076.0", "@aws-sdk/client-s3": "^3.1076.0", @@ -25,7 +26,7 @@ }, "packages/frontend": { "name": "@floci/frontend", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@tanstack/react-query": "^5.101.2", "@tanstack/react-query-devtools": "^5.101.2", @@ -65,6 +66,8 @@ "@aws-sdk/client-eks": ["@aws-sdk/client-eks@3.1080.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.28", "@aws-sdk/credential-provider-node": "^3.972.63", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/fetch-http-handler": "^5.6.2", "@smithy/node-http-handler": "^4.9.2", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-tN9O6f9vc9ns/uaPsN6vzEOrD7ftkWOMgzXoxY1J438WM2M8qcGbRJkhD2c+PvaZvZY3MVF7843mdIVrOA3zzQ=="], + "@aws-sdk/client-iam": ["@aws-sdk/client-iam@3.1096.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/credential-provider-node": "^3.972.73", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/fetch-http-handler": "^5.6.10", "@smithy/node-http-handler": "^4.9.10", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-39a+pXB0V9qWciaXsfPj4yhPNLBh69xiB6VG3auWCdj2PUW/oz1wjVOpuiH6mGUjWw8SMzVTcKrg0UaiUIPgXQ=="], + "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1080.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.28", "@aws-sdk/credential-provider-node": "^3.972.63", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/fetch-http-handler": "^5.6.2", "@smithy/node-http-handler": "^4.9.2", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-rTitNyvjgBrJzjDkzLRcLi1QcxSomomLRjhYXJWID97uaw1uxtmer8wOVi8l6UFxzlRVa83laZ3srAeWRs509A=="], "@aws-sdk/client-rds": ["@aws-sdk/client-rds@3.1080.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.28", "@aws-sdk/credential-provider-node": "^3.972.63", "@aws-sdk/middleware-sdk-rds": "^3.972.42", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/fetch-http-handler": "^5.6.2", "@smithy/node-http-handler": "^4.9.2", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-nkUu5ZN2TYTWslADZxMxNHMwl9NGdRGyWR+ZOPWL2MlDGbid3a2b7QAku6eaCoiDkWHonKbaHZYNtGoalkmyGA=="], @@ -533,6 +536,20 @@ "zustand": ["zustand@5.0.14", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g=="], + "@aws-sdk/client-iam/@aws-sdk/core": ["@aws-sdk/core@3.977.1", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.8", "@smithy/signature-v4": "^5.6.9", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-KVtQRtc00ES/y+Sc3vYXeP6pCIcNlBJCZOwvqSy8ZpVGmbM5+IG+AfhuTKQ2oXmIVqZJewaGMMpzPkywC6xg0w=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.73", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.62", "@aws-sdk/credential-provider-http": "^3.972.64", "@aws-sdk/credential-provider-ini": "^3.973.7", "@aws-sdk/credential-provider-process": "^3.972.62", "@aws-sdk/credential-provider-sso": "^3.973.6", "@aws-sdk/credential-provider-web-identity": "^3.972.68", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/credential-provider-imds": "^4.4.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-VTzdbf8Ukjdb9yUubZzRI678CWZvKovhE8Nv3qihwhC187sRMGls+r9N8Wuht5q1xjKx2nmpS48ar8ppupjkCA=="], + + "@aws-sdk/client-iam/@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], + + "@aws-sdk/client-iam/@smithy/core": ["@smithy/core@3.31.0", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-sylYk2l9d7CmRv8ts8p0SDQUr3VO+HMeS1nrjL6+UtbO8ktJHTOeQ1McX+aAyvGGccp5aZX9eNtdcXrSwzoZaw=="], + + "@aws-sdk/client-iam/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.0", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-OpQgP6IGH4j0NJ2zjfYZLjQL85ai+Wi/q51EmZJovXsEwKSvu89qiXUq77Q6EmwZ/hSl7fKpn2Z9mhiDN6OM+Q=="], + + "@aws-sdk/client-iam/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.12", "", { "dependencies": { "@smithy/core": "^3.31.0", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dWW5KRt4mnEvjNzbGqGeCuAvgum85Y9ZoyuMQqcTEfapndyVJ1k9BEHK7kdXJZ32enyRmmwcFjMwlB/KgLKI3Q=="], + + "@aws-sdk/client-iam/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + "@babel/helper-compilation-targets/browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -543,6 +560,24 @@ "bun-types/@types/node": ["@types/node@20.19.39", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw=="], + "@aws-sdk/client-iam/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], + + "@aws-sdk/client-iam/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.11", "", { "dependencies": { "@smithy/core": "^3.31.0", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-7HsspeiNCZvZHEJ22vV5L/QYuJdTyJvPJvMrYD3AgkM3IJB0pkln4jkjPvtpTWRMkHXbO8WKwNjoVdVlBFwHmw=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.62", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-BkDrk2cNjed31IKin/Oksb2ziF+gfuyRskFVuT4EU9Mep7M8Y/d8DJG4+anHme4Vuse7CwaEscwEfGyR6mzBhQ=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.64", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/fetch-http-handler": "^5.6.10", "@smithy/node-http-handler": "^4.9.10", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Wj1FGK2IxY5EccQCvH+niTYhIvDoDujJf2CpRRgS3NpYNEgiFNVItNbJYQjINRlu7fG7jSsXkKV0UWKriEplrw=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.7", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/credential-provider-env": "^3.972.62", "@aws-sdk/credential-provider-http": "^3.972.64", "@aws-sdk/credential-provider-login": "^3.972.69", "@aws-sdk/credential-provider-process": "^3.972.62", "@aws-sdk/credential-provider-sso": "^3.973.6", "@aws-sdk/credential-provider-web-identity": "^3.972.68", "@aws-sdk/nested-clients": "^3.997.36", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/credential-provider-imds": "^4.4.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-2CefB8cCxDu52P24B8Ay93/cTT199bcSvNHQ8e2f4BjSCF83yErBnTIZEBo0VeIgCfmw+PJKFUXnlQWxm2dkug=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.62", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zXYU9UWNL66gtMgNLhmxlrvEokuI7r6G2q7FRGu41Bya4iS30JLelUipJX9SV4zhyCPWJhI9Li54R1d9H8Tq6A=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.6", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/nested-clients": "^3.997.36", "@aws-sdk/token-providers": "3.1096.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-DobZggy3K49xdCpjeyMou0FQhkoYbluVGNydL6D+lcxF8GoAsttFX0xnH5GmiQ89We5dB6TRpW+CD/VowBH6HQ=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.68", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/nested-clients": "^3.997.36", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bq+yTt+uWJx60VVp/OIAX5xqUAu/K2Uc3eknWnWl+KtfcU2CQe0uNw6lySrn2t5GKHq7jsV0Z63HiBGVtzr/lg=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.15", "", { "dependencies": { "@smithy/core": "^3.31.0", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-xYVGrisQqTJWhOnScUhbx8s9H63TMtoxzuUoxG6mP8J+B/YbX3vZxVsgV0xDf43abJnJP0fjP7BkQh7OESwuRA=="], + "@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.10.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA=="], "@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="], @@ -552,5 +587,27 @@ "@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], "bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.69", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/nested-clients": "^3.997.36", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-gM3j0Ie9+FoLNTYODY+QWbg3vCRBc7mR9cRdntxTMkFYIrwfRmuucfavP6HNBlYSuaYww54TNJGej4GFgoPZAg=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.36", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/signature-v4-multi-region": "^3.996.42", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/fetch-http-handler": "^5.6.10", "@smithy/node-http-handler": "^4.9.10", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-b71Suv7L+DnhM0MsQHU4WO42I32kxLZi96PbVhZbxMYIoKnEZz3v+LSrG8fupAoA4cBSshCk1Dl/PeRz49qUSg=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.36", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/signature-v4-multi-region": "^3.996.42", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/fetch-http-handler": "^5.6.10", "@smithy/node-http-handler": "^4.9.10", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-b71Suv7L+DnhM0MsQHU4WO42I32kxLZi96PbVhZbxMYIoKnEZz3v+LSrG8fupAoA4cBSshCk1Dl/PeRz49qUSg=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1096.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/nested-clients": "^3.997.36", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hdUS2hDppy3vkWeFl5y86RLNU6OWH2mQB09yOSsRefwhhGTSFPkaZvfLDD/9vFcvMzlr8QFQFw3fw2FtrurVQA=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.36", "", { "dependencies": { "@aws-sdk/core": "^3.977.1", "@aws-sdk/signature-v4-multi-region": "^3.996.42", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.8", "@smithy/fetch-http-handler": "^5.6.10", "@smithy/node-http-handler": "^4.9.10", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-b71Suv7L+DnhM0MsQHU4WO42I32kxLZi96PbVhZbxMYIoKnEZz3v+LSrG8fupAoA4cBSshCk1Dl/PeRz49qUSg=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.42", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.9", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-DBV4naZP6HYBlAvPpoQzOP12Wvfou/5rN8yJPXjBTBylU5qwCbh/tXr2MddHoIjgoRkEl/eS+IljiUqvmwey1Q=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.42", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.9", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-DBV4naZP6HYBlAvPpoQzOP12Wvfou/5rN8yJPXjBTBylU5qwCbh/tXr2MddHoIjgoRkEl/eS+IljiUqvmwey1Q=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.42", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.9", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-DBV4naZP6HYBlAvPpoQzOP12Wvfou/5rN8yJPXjBTBylU5qwCbh/tXr2MddHoIjgoRkEl/eS+IljiUqvmwey1Q=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.11", "", { "dependencies": { "@smithy/core": "^3.31.0", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-7HsspeiNCZvZHEJ22vV5L/QYuJdTyJvPJvMrYD3AgkM3IJB0pkln4jkjPvtpTWRMkHXbO8WKwNjoVdVlBFwHmw=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.11", "", { "dependencies": { "@smithy/core": "^3.31.0", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-7HsspeiNCZvZHEJ22vV5L/QYuJdTyJvPJvMrYD3AgkM3IJB0pkln4jkjPvtpTWRMkHXbO8WKwNjoVdVlBFwHmw=="], + + "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.11", "", { "dependencies": { "@smithy/core": "^3.31.0", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-7HsspeiNCZvZHEJ22vV5L/QYuJdTyJvPJvMrYD3AgkM3IJB0pkln4jkjPvtpTWRMkHXbO8WKwNjoVdVlBFwHmw=="], } } diff --git a/packages/api/package.json b/packages/api/package.json index 4c60859..a3044a6 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -12,6 +12,7 @@ "dependencies": { "@aws-sdk/client-ec2": "^3.1076.0", "@aws-sdk/client-eks": "^3.1076.0", + "@aws-sdk/client-iam": "^3.1096.0", "@aws-sdk/client-lambda": "^3.1076.0", "@aws-sdk/client-rds": "^3.1076.0", "@aws-sdk/client-s3": "^3.1076.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f04df5d..5cf5a85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,9 @@ importers: '@aws-sdk/client-eks': specifier: ^3.1076.0 version: 3.1076.0 + '@aws-sdk/client-iam': + specifier: ^3.1096.0 + version: 3.1096.0 '@aws-sdk/client-lambda': specifier: ^3.1076.0 version: 3.1076.0 @@ -157,6 +160,10 @@ packages: resolution: {integrity: sha512-Hqr3hdwUgqWAQbdx6UDy81UBF+jdu3ws6RrykQ4YmBhe/XOKv57lt2lhkjgBuF9fiSw4UUl2XQteks9oxgm4XA==} engines: {node: '>=20.0.0'} + '@aws-sdk/client-iam@3.1096.0': + resolution: {integrity: sha512-39a+pXB0V9qWciaXsfPj4yhPNLBh69xiB6VG3auWCdj2PUW/oz1wjVOpuiH6mGUjWw8SMzVTcKrg0UaiUIPgXQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/client-lambda@3.1076.0': resolution: {integrity: sha512-uueu0Do1dy3cm42AdjL3zivHoc5b55vOL3g6BjxUv2xMifSLKZi5Ilh5FhXVDdS/Uwxak/JH+vVQDDnEuQ52gA==} engines: {node: '>=20.0.0'} @@ -177,38 +184,74 @@ packages: resolution: {integrity: sha512-4/1DtLwgLqzIg2uFzkFaFjMQHhhhwHIZN4PfziIVqXYX7koO78omuchQlLHyzQBw80l255dtmTA/J4W5yhb7zw==} engines: {node: '>=20.0.0'} + '@aws-sdk/core@3.977.1': + resolution: {integrity: sha512-KVtQRtc00ES/y+Sc3vYXeP6pCIcNlBJCZOwvqSy8ZpVGmbM5+IG+AfhuTKQ2oXmIVqZJewaGMMpzPkywC6xg0w==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-env@3.972.54': resolution: {integrity: sha512-F4WQCG8GULIt+XrMHsqUM9dZc0eTwZM3HUWByOjIKOwBqJSzUxX8CFAtbgMvWfCua52FS1oi5FXarH3+khFq8A==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-env@3.972.62': + resolution: {integrity: sha512-BkDrk2cNjed31IKin/Oksb2ziF+gfuyRskFVuT4EU9Mep7M8Y/d8DJG4+anHme4Vuse7CwaEscwEfGyR6mzBhQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-http@3.972.56': resolution: {integrity: sha512-PiwHgEK2srdfi/lFveyQ+3w/qikyh6MKvuZAdlMC+dwQi4K5iVBr32oh7cugnYw+jA8iGtnQMhCGvLm/mqplmw==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-http@3.972.64': + resolution: {integrity: sha512-Wj1FGK2IxY5EccQCvH+niTYhIvDoDujJf2CpRRgS3NpYNEgiFNVItNbJYQjINRlu7fG7jSsXkKV0UWKriEplrw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-ini@3.972.61': resolution: {integrity: sha512-9CZWMjhBlgfKlqJk40R7kvMOLEIoOr3HJ1J/ELjAH9H5LzR4bCxLujPVM/1PKAEjzSZKZCxWOalm7JUGZbhQqw==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-ini@3.973.7': + resolution: {integrity: sha512-2CefB8cCxDu52P24B8Ay93/cTT199bcSvNHQ8e2f4BjSCF83yErBnTIZEBo0VeIgCfmw+PJKFUXnlQWxm2dkug==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-login@3.972.60': resolution: {integrity: sha512-Ak6OOrCbXvACyxLFIP1mcS+JTLS9ZpW1ZqyBtqu6axvdpsbG1gVNhUlAfQq8TK/gar/h2w35LrzlQU0PcUzJpw==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-login@3.972.69': + resolution: {integrity: sha512-gM3j0Ie9+FoLNTYODY+QWbg3vCRBc7mR9cRdntxTMkFYIrwfRmuucfavP6HNBlYSuaYww54TNJGej4GFgoPZAg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.63': resolution: {integrity: sha512-YmgWtTPZDStyT74ApSHpApD3r7W9znsc+WEZjW0vceom+NAxRx9/F3TyukOKix8kJkPaa49aIREQJdpGeLDiEw==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.73': + resolution: {integrity: sha512-VTzdbf8Ukjdb9yUubZzRI678CWZvKovhE8Nv3qihwhC187sRMGls+r9N8Wuht5q1xjKx2nmpS48ar8ppupjkCA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-process@3.972.54': resolution: {integrity: sha512-UNmUjtTnp3wH3YTZcctN7yK17S2AdaJpiNIdIf0l70hHOdGuDfdMxk62P5rt64GwGm6qkFOYG0js/cU6cdZDdg==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-process@3.972.62': + resolution: {integrity: sha512-zXYU9UWNL66gtMgNLhmxlrvEokuI7r6G2q7FRGu41Bya4iS30JLelUipJX9SV4zhyCPWJhI9Li54R1d9H8Tq6A==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-sso@3.972.60': resolution: {integrity: sha512-zH8SvJkTRw1Kb7GARjGPjzkQPfL3jDi/OxK32I5SQq7bgwgFHJZaoDEwkEvFlfNfpByM6n4Rs20Y1mLyOdb6ag==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-sso@3.973.6': + resolution: {integrity: sha512-DobZggy3K49xdCpjeyMou0FQhkoYbluVGNydL6D+lcxF8GoAsttFX0xnH5GmiQ89We5dB6TRpW+CD/VowBH6HQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.60': resolution: {integrity: sha512-q2rJSQ/AMjemUS18OtQqczqSo2R6VjOCLmLVmLVcdrP5/AKoj632lGCrMUWQ6EgnaLMEHbq0UO4mWh/u6gjPhA==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.68': + resolution: {integrity: sha512-bq+yTt+uWJx60VVp/OIAX5xqUAu/K2Uc3eknWnWl+KtfcU2CQe0uNw6lySrn2t5GKHq7jsV0Z63HiBGVtzr/lg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-flexible-checksums@3.974.38': resolution: {integrity: sha512-grguw9ssx2zVIn8/KpgQ7AQLjBE/YPwOns94n0seWkNWS3dJ7k7VFD2voAkh9Xvf8Azxw8Ajc5+KicdClucMqQ==} engines: {node: '>=20.0.0'} @@ -229,18 +272,34 @@ packages: resolution: {integrity: sha512-1oG/mM3jmE2M3kad2zWJS6IKIY8hjRV4l5kAgg+xTQdLMTtehhcSucL/y4WqQpcHmQwi6+gRK8E46GYl1NBW9Q==} engines: {node: '>=20.0.0'} + '@aws-sdk/nested-clients@3.997.36': + resolution: {integrity: sha512-b71Suv7L+DnhM0MsQHU4WO42I32kxLZi96PbVhZbxMYIoKnEZz3v+LSrG8fupAoA4cBSshCk1Dl/PeRz49qUSg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/signature-v4-multi-region@3.996.38': resolution: {integrity: sha512-C379Sk+MiFZCfWZphKlMyLHKxV22OjoGM5KJjj5IJNJcOCWL4IGIpnEGzv1FQiRwhYXfq55SJMfxlqPE08JJ9g==} engines: {node: '>=20.0.0'} + '@aws-sdk/signature-v4-multi-region@3.996.42': + resolution: {integrity: sha512-DBV4naZP6HYBlAvPpoQzOP12Wvfou/5rN8yJPXjBTBylU5qwCbh/tXr2MddHoIjgoRkEl/eS+IljiUqvmwey1Q==} + engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1080.0': resolution: {integrity: sha512-8PufAQvncWXvdZUvODbuyXa8l3aszefEzwSMBUcgheNbZOmJMcNn388Ebt/piVrUHyxKN1MlxnR2OonzTyZaGw==} engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1096.0': + resolution: {integrity: sha512-hdUS2hDppy3vkWeFl5y86RLNU6OWH2mQB09yOSsRefwhhGTSFPkaZvfLDD/9vFcvMzlr8QFQFw3fw2FtrurVQA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.973.15': resolution: {integrity: sha512-IULn8uBV/SMtmOIANsm4WHXIOtVPBWfOWs3WGL0j/sI+KhaYehvOw0ET+9urnn8MBpiijuU/0JOpuwKOE451PQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.974.2': + resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/util-locate-window@3.965.8': resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} engines: {node: '>=20.0.0'} @@ -249,6 +308,10 @@ packages: resolution: {integrity: sha512-ezbwz9WpuLctm6o7P2t2naDhVVPI5jFGrVefVybhcKGjU57VIyT46pQVO0RI2RYkUdhdj2Z9uSIlAzGZE9NW9A==} engines: {node: '>=20.0.0'} + '@aws-sdk/xml-builder@3.972.37': + resolution: {integrity: sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==} + engines: {node: '>=20.0.0'} + '@aws/lambda-invoke-store@0.3.0': resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} engines: {node: '>=18.0.0'} @@ -509,10 +572,22 @@ packages: resolution: {integrity: sha512-qoiY4nrk5OCu1+eIR1VB8l5DmON/oKiqrd5zZFAhXJXjJlLWQusKEW/SkBDAtGDcPaz86m9kfcE1lngU0GlM6A==} engines: {node: '>=18.0.0'} + '@smithy/core@3.31.0': + resolution: {integrity: sha512-sylYk2l9d7CmRv8ts8p0SDQUr3VO+HMeS1nrjL6+UtbO8ktJHTOeQ1McX+aAyvGGccp5aZX9eNtdcXrSwzoZaw==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.15': + resolution: {integrity: sha512-xYVGrisQqTJWhOnScUhbx8s9H63TMtoxzuUoxG6mP8J+B/YbX3vZxVsgV0xDf43abJnJP0fjP7BkQh7OESwuRA==} + engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.4.6': resolution: {integrity: sha512-B2WQ/PV/H6Jeg3lrIq6bKUfa6Hy01mtK7CGs6lhjzHA6k4aagldH6T6eEjnzKl4HI0cJnAsxfJ19pgb5PV+CVQ==} engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.6.12': + resolution: {integrity: sha512-OpQgP6IGH4j0NJ2zjfYZLjQL85ai+Wi/q51EmZJovXsEwKSvu89qiXUq77Q6EmwZ/hSl7fKpn2Z9mhiDN6OM+Q==} + engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.6.3': resolution: {integrity: sha512-CwCc/7SMTj45y97MUnDTbTaxvtAsiNNRm81z3abROIuMbMsC2Iy5EKfkkVdsKrz8WExQAAMx1EJapq+9j4fFTQ==} engines: {node: '>=18.0.0'} @@ -521,10 +596,18 @@ packages: resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} + '@smithy/node-http-handler@4.9.12': + resolution: {integrity: sha512-dWW5KRt4mnEvjNzbGqGeCuAvgum85Y9ZoyuMQqcTEfapndyVJ1k9BEHK7kdXJZ32enyRmmwcFjMwlB/KgLKI3Q==} + engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.9.3': resolution: {integrity: sha512-qZTa4gQFUo8RM02rk6q5UVTDLNrQ1oS20LsepBzqq1QBVc/EHJ03OOUADcqMZiXHArW+Y7+OGY0BpdTwZRq/Yg==} engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.6.11': + resolution: {integrity: sha512-7HsspeiNCZvZHEJ22vV5L/QYuJdTyJvPJvMrYD3AgkM3IJB0pkln4jkjPvtpTWRMkHXbO8WKwNjoVdVlBFwHmw==} + engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.6.2': resolution: {integrity: sha512-QgHflghMoPxCJ9axiCVh8KZfbC9fuP6vkXXyK//E3cq7nLaSSyyLj0GAoqVWezYeDQmXIZhmlRvLE16jsqDK6g==} engines: {node: '>=18.0.0'} @@ -533,6 +616,10 @@ packages: resolution: {integrity: sha512-x3L0XSACF6UYzKpa9biqiRMgvH5+wnFFew9Tm/grFYqgaupPwx/+ojDPpPJM8dZON3S9tjz5U+PQYsCBd1Mw5Q==} engines: {node: '>=18.0.0'} + '@smithy/types@4.16.1': + resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} + engines: {node: '>=18.0.0'} + '@smithy/util-buffer-from@2.2.0': resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} @@ -1341,6 +1428,17 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/client-iam@3.1096.0': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/credential-provider-node': 3.972.73 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/fetch-http-handler': 5.6.12 + '@smithy/node-http-handler': 4.9.12 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/client-lambda@3.1076.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -1409,6 +1507,17 @@ snapshots: bowser: 2.14.1 tslib: 2.8.1 + '@aws-sdk/core@3.977.1': + dependencies: + '@aws-sdk/types': 3.974.2 + '@aws-sdk/xml-builder': 3.972.37 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.31.0 + '@smithy/signature-v4': 5.6.11 + '@smithy/types': 4.16.1 + bowser: 2.14.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.972.54': dependencies: '@aws-sdk/core': 3.974.28 @@ -1417,6 +1526,14 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.972.62': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.972.56': dependencies: '@aws-sdk/core': 3.974.28 @@ -1427,6 +1544,16 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.972.64': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/fetch-http-handler': 5.6.12 + '@smithy/node-http-handler': 4.9.12 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.972.61': dependencies: '@aws-sdk/core': 3.974.28 @@ -1443,6 +1570,22 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.973.7': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/credential-provider-env': 3.972.62 + '@aws-sdk/credential-provider-http': 3.972.64 + '@aws-sdk/credential-provider-login': 3.972.69 + '@aws-sdk/credential-provider-process': 3.972.62 + '@aws-sdk/credential-provider-sso': 3.973.6 + '@aws-sdk/credential-provider-web-identity': 3.972.68 + '@aws-sdk/nested-clients': 3.997.36 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/credential-provider-imds': 4.4.15 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-login@3.972.60': dependencies: '@aws-sdk/core': 3.974.28 @@ -1452,6 +1595,15 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-login@3.972.69': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/nested-clients': 3.997.36 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-node@3.972.63': dependencies: '@aws-sdk/credential-provider-env': 3.972.54 @@ -1466,6 +1618,20 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-node@3.972.73': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.62 + '@aws-sdk/credential-provider-http': 3.972.64 + '@aws-sdk/credential-provider-ini': 3.973.7 + '@aws-sdk/credential-provider-process': 3.972.62 + '@aws-sdk/credential-provider-sso': 3.973.6 + '@aws-sdk/credential-provider-web-identity': 3.972.68 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/credential-provider-imds': 4.4.15 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-process@3.972.54': dependencies: '@aws-sdk/core': 3.974.28 @@ -1474,6 +1640,14 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-process@3.972.62': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-sso@3.972.60': dependencies: '@aws-sdk/core': 3.974.28 @@ -1484,6 +1658,16 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-sso@3.973.6': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/nested-clients': 3.997.36 + '@aws-sdk/token-providers': 3.1096.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-web-identity@3.972.60': dependencies: '@aws-sdk/core': 3.974.28 @@ -1493,6 +1677,15 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-web-identity@3.972.68': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/nested-clients': 3.997.36 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/middleware-flexible-checksums@3.974.38': dependencies: '@aws-sdk/checksums': 3.1000.13 @@ -1536,6 +1729,17 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/nested-clients@3.997.36': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/signature-v4-multi-region': 3.996.42 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/fetch-http-handler': 5.6.12 + '@smithy/node-http-handler': 4.9.12 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.996.38': dependencies: '@aws-sdk/types': 3.973.15 @@ -1543,6 +1747,13 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.996.42': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/signature-v4': 5.6.11 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.1080.0': dependencies: '@aws-sdk/core': 3.974.28 @@ -1552,11 +1763,25 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/token-providers@3.1096.0': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/nested-clients': 3.997.36 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/types@3.973.15': dependencies: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/types@3.974.2': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/util-locate-window@3.965.8': dependencies: tslib: 2.8.1 @@ -1566,6 +1791,11 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.37': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws/lambda-invoke-store@0.3.0': {} '@babel/code-frame@7.29.0': @@ -1818,12 +2048,29 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@smithy/core@3.31.0': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.15': + dependencies: + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@smithy/credential-provider-imds@4.4.6': dependencies: '@smithy/core': 3.29.1 '@smithy/types': 4.15.1 tslib: 2.8.1 + '@smithy/fetch-http-handler@5.6.12': + dependencies: + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@smithy/fetch-http-handler@5.6.3': dependencies: '@smithy/core': 3.29.1 @@ -1834,12 +2081,24 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/node-http-handler@4.9.12': + dependencies: + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@smithy/node-http-handler@4.9.3': dependencies: '@smithy/core': 3.29.1 '@smithy/types': 4.15.1 tslib: 2.8.1 + '@smithy/signature-v4@5.6.11': + dependencies: + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@smithy/signature-v4@5.6.2': dependencies: '@smithy/core': 3.29.1 @@ -1850,6 +2109,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/types@4.16.1': + dependencies: + tslib: 2.8.1 + '@smithy/util-buffer-from@2.2.0': dependencies: '@smithy/is-array-buffer': 2.2.0 From ebd568d2e14e682ce022d4549aa573a885ec4938 Mon Sep 17 00:00:00 2001 From: TheSaifZaman Date: Tue, 28 Jul 2026 16:05:06 +0600 Subject: [PATCH 2/6] feat(api): let a schema declare query facets beyond search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `ResourceQuery.filters`, populated by the list route from query params — but only for the facet names the service's own schema declares in `filters`. An undeclared or stale param is ignored rather than reaching an adapter that never asked for it, the same way the service catalog gates unknown service slugs. Free-text search cannot express "only roles", which is what a category holding several kinds of resource needs. `search` stays a separate field on ResourceQuery, so every existing adapter is untouched. --- packages/api/src/cloud-spi/types.ts | 12 +++++- packages/api/src/routes/clouds.test.ts | 58 +++++++++++++++++++++++++- packages/api/src/routes/clouds.ts | 31 +++++++++++++- 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/packages/api/src/cloud-spi/types.ts b/packages/api/src/cloud-spi/types.ts index 69ea0ba..66fc4e9 100644 --- a/packages/api/src/cloud-spi/types.ts +++ b/packages/api/src/cloud-spi/types.ts @@ -152,7 +152,7 @@ export interface CloudResource { name: string cloud: CloudProvider service: CloudServiceType - type: 'bucket' | 'container' | 'cluster' | 'db-instance' | 'cosmos-database' | 'instance' | 'image' | 'vpc' | 'lambda' | 'azure-function' | 'gcp-function' | 'secret' + type: 'bucket' | 'container' | 'cluster' | 'db-instance' | 'cosmos-database' | 'instance' | 'image' | 'vpc' | 'lambda' | 'azure-function' | 'gcp-function' | 'secret' | 'iam-user' | 'iam-role' | 'iam-policy' region: string | null createdAt: string | null status?: string | null @@ -208,6 +208,16 @@ export interface CosmosQueryResult { export interface ResourceQuery { search?: string + /** + * Values for the non-search facets a schema declares in `filters`. + * + * Needed because some services hold several kinds of resource in one category + * — IAM lists users, roles and policies — and a free-text search cannot + * express "only roles". The route populates this from query params, but only + * for names the service's own schema declares, so an unknown param is ignored + * rather than reaching an adapter that never asked for it. + */ + filters?: Record } export interface CreateResourceInput { diff --git a/packages/api/src/routes/clouds.test.ts b/packages/api/src/routes/clouds.test.ts index 11c6881..d241796 100644 --- a/packages/api/src/routes/clouds.test.ts +++ b/packages/api/src/routes/clouds.test.ts @@ -3,7 +3,7 @@ import {Hono} from 'hono' import {NotImplementedByRuntimeError, RuntimeUnavailableError, ValidationError} from '../cloud-spi/errors' import {azureDatabaseSchema} from '../cloud-spi/databaseSchema' import {awsStorageSchema, azureStorageSchema, gcpStorageSchema} from '../cloud-spi/storageSchema' -import type {CloudProvider, CloudResource, CloudServiceAdapter, CosmosContainer, CosmosItem, CosmosQueryResult, CreateResourceInput} from '../cloud-spi/types' +import type {CloudProvider, CloudResource, CloudServiceAdapter, CosmosContainer, CosmosItem, CosmosQueryResult, CreateResourceInput, FieldSchema, ResourceQuery} from '../cloud-spi/types' import {CloudAdapterRegistry} from '../registry/CloudAdapterRegistry' import {CloudProxyService} from '../service/CloudProxyService' import type {RuntimeProbe} from '../service/runtimeProbe' @@ -68,6 +68,62 @@ function appWithRoutes( return app } +describe('list query facets', () => { + /** Captures the ResourceQuery the route hands to the adapter. */ + function capturingApp( + filters: FieldSchema[] = [{name: 'search', label: 'Search', type: 'text', required: false}], + ) { + const seen: ResourceQuery[] = [] + const adapter = mockAdapter('aws', { + schema: () => ({...awsStorageSchema(), filters}), + list: async (query: ResourceQuery = {}) => { + seen.push(query) + return [] + }, + }) + return {app: appWithRoutes([adapter]), seen} + } + + const kindFilter: FieldSchema = { + name: 'kind', + label: 'Kind', + type: 'select', + required: false, + options: [{label: 'Roles', value: 'roles'}], + } + + test('passes search through as before', async () => { + const {app, seen} = capturingApp() + await app.request('/api/clouds/aws/services/storage/resources?search=web') + + expect(seen[0]?.search).toBe('web') + expect(seen[0]?.filters).toBeUndefined() + }) + + test('passes a facet the schema declares', async () => { + const {app, seen} = capturingApp([kindFilter]) + await app.request('/api/clouds/aws/services/storage/resources?kind=roles') + + expect(seen[0]?.filters).toEqual({kind: 'roles'}) + }) + + test('ignores a query param the schema does not declare', async () => { + // Otherwise a stale or hand-crafted param would reach an adapter that + // never asked for it, and the adapter would have to defend itself. + const {app, seen} = capturingApp([kindFilter]) + await app.request('/api/clouds/aws/services/storage/resources?kind=roles&sneaky=1') + + expect(seen[0]?.filters).toEqual({kind: 'roles'}) + }) + + test('omits filters entirely when a declared facet is absent or blank', async () => { + const {app, seen} = capturingApp([kindFilter]) + await app.request('/api/clouds/aws/services/storage/resources?kind=') + + expect(seen[0]?.filters).toBeUndefined() + }) +}) + describe('cloud schema routes', () => { test('returns AWS storage schema', async () => { const res = await appWithRoutes().request('/api/clouds/aws/services/storage/schema') diff --git a/packages/api/src/routes/clouds.ts b/packages/api/src/routes/clouds.ts index 5ba7a25..e14df63 100644 --- a/packages/api/src/routes/clouds.ts +++ b/packages/api/src/routes/clouds.ts @@ -1,6 +1,6 @@ import {Hono} from 'hono' import type {Context} from 'hono' -import type {CloudProvider, CloudServiceType} from '../cloud-spi/types' +import type {CloudProvider, CloudServiceType, ServiceSchema} from '../cloud-spi/types' import {toHttpError} from '../cloud-spi/errors' import {isServiceType} from '../cloud-spi/serviceCatalog' import {mapAwsSdkError} from '../adapter-aws/awsErrors' @@ -61,7 +61,11 @@ export function createCloudRoutes(injectedService?: CloudProxyService) { if (!isCloudProvider(cloud) || !isServiceType(serviceType)) return c.json({error: 'Unknown cloud or service'}, 404) return withRuntime(c, async () => { - const resources = await svc(c).listResources(cloud, serviceType, {search: c.req.query('search')}) + const service = svc(c) + const resources = await service.listResources(cloud, serviceType, { + search: c.req.query('search'), + filters: declaredFilters(service.schema(cloud, serviceType), (name) => c.req.query(name)), + }) return c.json(resources) }) }) @@ -281,4 +285,27 @@ async function withRuntime(c: Context, handler: () => Promise): Promis } } +/** + * Collect values for the facets a service's schema actually declares. + * + * Driven by the schema rather than by "every query param that is not `search`", + * so a stray or stale param never reaches an adapter that did not ask for it — + * the same reason the service catalog gates unknown service slugs. `search` is + * excluded because it is carried separately on `ResourceQuery`. + */ +function declaredFilters( + schema: ServiceSchema | null, + valueFor: (name: string) => string | undefined, +): Record | undefined { + const filters: Record = {} + + for (const filter of schema?.filters ?? []) { + if (filter.name === 'search') continue + const value = valueFor(filter.name) + if (value !== undefined && value !== '') filters[filter.name] = value + } + + return Object.keys(filters).length > 0 ? filters : undefined +} + export default createCloudRoutes() From da7d8bab73bb2b092c77d63fee4212652413cff5 Mon Sep 17 00:00:00 2001 From: TheSaifZaman Date: Tue, 28 Jul 2026 16:05:06 +0600 Subject: [PATCH 3/6] feat(iam): add an AWS IAM adapter with a kind facet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an `iam` service under Security covering users, roles and customer-managed policies, and is the first consumer of the schema-declared query facets added in the previous commit. The facet is the point: IAM holds three kinds of resource in one category, and `kind=roles` is not something a search box can express. Unset, list returns all three together, which is what an audit view wants. Decisions that follow from the API's shape: - Resource ids are `kind/identifier` — user/alice, role/deployer, and policy/{arn} for policies, because GetPolicy and DeletePolicy take an ARN rather than a name. A bare name would be ambiguous across kinds and would not tell get or delete which API to call. The ids survive the generic route because HttpClient encodes path params. - Policies are listed with Scope=Local. Real IAM otherwise returns close to a thousand AWS-managed policies and buries the account's own; the emulator returns few enough that only a test can hold this line. - A role's trust policy is decoded from the percent-encoded form IAM returns, which is unreadable raw in the inspector. - create validates per kind: a role needs a trust policy and a policy needs a document, both of which must parse as JSON. The message names the field and the kind, because the form shows both fields for every kind and a flat form cannot mark them conditionally required. - Every List call pages on IsTruncated/Marker. Verified end to end through the route: the nav entry, create for all three kinds, list with and without the facet, an undeclared query param being ignored, an unknown facet value returning 400, inspect by encoded id including a policy ARN, the decoded trust policy, delete for each kind, and 404 for a missing entity. --- .../api/src/adapter-aws/AwsIamAdapter.test.ts | 294 +++++++++++++++ packages/api/src/adapter-aws/AwsIamAdapter.ts | 356 ++++++++++++++++++ packages/api/src/aws.ts | 3 + packages/api/src/cloud-spi/iamSchema.ts | 103 +++++ packages/api/src/cloud-spi/serviceCatalog.ts | 6 + packages/api/src/cloudProxy.ts | 2 + 6 files changed, 764 insertions(+) create mode 100644 packages/api/src/adapter-aws/AwsIamAdapter.test.ts create mode 100644 packages/api/src/adapter-aws/AwsIamAdapter.ts create mode 100644 packages/api/src/cloud-spi/iamSchema.ts diff --git a/packages/api/src/adapter-aws/AwsIamAdapter.test.ts b/packages/api/src/adapter-aws/AwsIamAdapter.test.ts new file mode 100644 index 0000000..d896b3c --- /dev/null +++ b/packages/api/src/adapter-aws/AwsIamAdapter.test.ts @@ -0,0 +1,294 @@ +import {describe, expect, test} from 'bun:test' +import { + CreatePolicyCommand, + CreateRoleCommand, + CreateUserCommand, + DeletePolicyCommand, + DeleteRoleCommand, + DeleteUserCommand, + GetPolicyCommand, + GetRoleCommand, + GetUserCommand, + type IAMClient, + ListPoliciesCommand, + ListRolesCommand, + ListUsersCommand, +} from '@aws-sdk/client-iam' +import {AwsIamAdapter} from './AwsIamAdapter' +import {ValidationError} from '../cloud-spi/errors' + +type SendResult = Record + +function stubIam(handler: (command: object) => SendResult | Promise) { + const sent: object[] = [] + const client = { + async send(command: object) { + sent.push(command) + return handler(command) + }, + } as unknown as IAMClient + return {client, sent} +} + +const user = { + UserName: 'alice', + UserId: 'AIDA99A311H4GBZ3N9UX', + Arn: 'arn:aws:iam::000000000000:user/alice', + Path: '/', + CreateDate: new Date('2026-07-28T10:00:00.000Z'), +} + +const role = { + RoleName: 'deployer', + RoleId: 'AROAPZ9FQ6LJB8HKPRID', + Arn: 'arn:aws:iam::000000000000:role/deployer', + Path: '/service/', + CreateDate: new Date('2026-07-28T11:00:00.000Z'), + AssumeRolePolicyDocument: '%7B%22Version%22%3A%222012-10-17%22%7D', + Description: 'CI deploy role', +} + +const policy = { + PolicyName: 'read-buckets', + PolicyId: 'ANPAJ2UCCR6DPCEXAMPLE', + Arn: 'arn:aws:iam::000000000000:policy/read-buckets', + Path: '/', + CreateDate: new Date('2026-07-28T12:00:00.000Z'), + AttachmentCount: 2, + DefaultVersionId: 'v1', +} + +/** Answers each List/Get command the way the runtime does. */ +function runtimeStub() { + return stubIam((command) => { + if (command instanceof ListUsersCommand) return {Users: [user], IsTruncated: false} + if (command instanceof ListRolesCommand) return {Roles: [role], IsTruncated: false} + if (command instanceof ListPoliciesCommand) return {Policies: [policy], IsTruncated: false} + if (command instanceof GetUserCommand) return {User: user} + if (command instanceof GetRoleCommand) return {Role: role} + if (command instanceof GetPolicyCommand) return {Policy: policy} + if (command instanceof CreateUserCommand) return {User: user} + if (command instanceof CreateRoleCommand) return {Role: role} + if (command instanceof CreatePolicyCommand) return {Policy: policy} + return {} + }) +} + +describe('AwsIamAdapter', () => { + test('identifies itself as the AWS IAM adapter', () => { + const adapter = new AwsIamAdapter(runtimeStub().client) + + expect(adapter.cloud).toBe('aws') + expect(adapter.service).toBe('iam') + expect(adapter.schema().displayName).toBe('AWS IAM') + }) + + test('lists all three kinds when no facet is given', async () => { + const {client} = runtimeStub() + const resources = await new AwsIamAdapter(client).list() + + // Policies are keyed by ARN, because GetPolicy and DeletePolicy take one. + expect(resources.map((r) => r.id)).toEqual([ + 'user/alice', + 'role/deployer', + `policy/${policy.Arn}`, + ]) + expect(resources.map((r) => r.type)).toEqual(['iam-user', 'iam-role', 'iam-policy']) + }) + + describe('the kind facet narrows the listing', () => { + const cases: Array<[string, string, unknown]> = [ + ['users', 'user/alice', ListUsersCommand], + ['roles', 'role/deployer', ListRolesCommand], + ['policies', `policy/${policy.Arn}`, ListPoliciesCommand], + ] + + for (const [kind, expectedId, command] of cases) { + test(`kind=${kind} lists only ${kind}`, async () => { + const {client, sent} = runtimeStub() + const resources = await new AwsIamAdapter(client).list({filters: {kind}}) + + expect(resources.map((r) => r.id)).toEqual([expectedId]) + expect(sent).toHaveLength(1) + expect(sent[0]).toBeInstanceOf(command as never) + }) + } + }) + + test('rejects a kind the schema does not offer', async () => { + const {client} = runtimeStub() + + await expect(new AwsIamAdapter(client).list({filters: {kind: 'wizards'}})).rejects.toThrow(ValidationError) + }) + + test('only asks for customer-managed policies', async () => { + // Real IAM returns close to a thousand AWS-managed policies without + // Scope=Local, which would bury the account's own policies. + const {client, sent} = runtimeStub() + await new AwsIamAdapter(client).list({filters: {kind: 'policies'}}) + + expect((sent[0] as ListPoliciesCommand).input.Scope).toBe('Local') + }) + + test('maps a user, a role and a policy onto the shared shape', async () => { + const {client} = runtimeStub() + const [asUser, asRole, asPolicy] = await new AwsIamAdapter(client).list() + + expect(asUser).toMatchObject({ + id: 'user/alice', + name: 'alice', + type: 'iam-user', + createdAt: '2026-07-28T10:00:00.000Z', + }) + expect(asUser?.metadata).toMatchObject({arn: user.Arn, path: '/', userId: user.UserId}) + expect(asRole?.metadata).toMatchObject({path: '/service/', description: 'CI deploy role'}) + expect(asPolicy?.metadata).toMatchObject({attachmentCount: 2, defaultVersionId: 'v1'}) + }) + + test('decodes the URL-encoded trust policy the runtime returns', async () => { + // IAM returns AssumeRolePolicyDocument percent-encoded; showing that raw in + // the inspector is unreadable. + const {client} = runtimeStub() + const resource = await new AwsIamAdapter(client).get('role/deployer') + + expect(resource?.metadata.assumeRolePolicyDocument).toBe('{"Version":"2012-10-17"}') + }) + + test('follows the pagination marker', async () => { + let call = 0 + const {client, sent} = stubIam((command) => { + if (!(command instanceof ListUsersCommand)) return {} + call += 1 + if (call === 1) return {Users: [user], IsTruncated: true, Marker: 'page-2'} + return {Users: [{...user, UserName: 'bob'}], IsTruncated: false} + }) + + const resources = await new AwsIamAdapter(client).list({filters: {kind: 'users'}}) + + expect(resources.map((r) => r.name)).toEqual(['alice', 'bob']) + expect((sent[1] as ListUsersCommand).input.Marker).toBe('page-2') + }) + + test('filters by search across name and arn', async () => { + const {client} = runtimeStub() + const adapter = new AwsIamAdapter(client) + + await expect(adapter.list({search: 'alic'})).resolves.toHaveLength(1) + await expect(adapter.list({search: 'role/deployer'})).resolves.toHaveLength(1) + await expect(adapter.list({search: 'nope'})).resolves.toHaveLength(0) + }) + + describe('inspect addresses a resource by kind and identifier', () => { + const cases: Array<[string, unknown]> = [ + ['user/alice', GetUserCommand], + ['role/deployer', GetRoleCommand], + ['policy/arn:aws:iam::000000000000:policy/read-buckets', GetPolicyCommand], + ] + + for (const [id, command] of cases) { + test(`${id.split('/')[0]} uses ${(command as {name: string}).name}`, async () => { + const {client, sent} = runtimeStub() + const resource = await new AwsIamAdapter(client).get(id) + + expect(sent[0]).toBeInstanceOf(command as never) + expect(resource).not.toBeNull() + }) + } + }) + + test('rejects an id that does not name a kind', async () => { + const {client} = runtimeStub() + + await expect(new AwsIamAdapter(client).get('alice')).rejects.toThrow(ValidationError) + await expect(new AwsIamAdapter(client).get('wizard/merlin')).rejects.toThrow(ValidationError) + }) + + test('returns null when an entity does not exist', async () => { + const {client} = stubIam(() => { + throw Object.assign(new Error('NoSuchEntity'), { + name: 'NoSuchEntity', + $metadata: {httpStatusCode: 404}, + }) + }) + await expect(new AwsIamAdapter(client).get('user/ghost')).resolves.toBeNull() + }) + + test('creates a user', async () => { + const {client, sent} = runtimeStub() + const resource = await new AwsIamAdapter(client).create({values: {kind: 'users', name: 'alice'}}) + + expect(sent[0]).toBeInstanceOf(CreateUserCommand) + expect((sent[0] as CreateUserCommand).input.UserName).toBe('alice') + expect(resource.id).toBe('user/alice') + }) + + test('creates a role with its trust policy', async () => { + const {client, sent} = runtimeStub() + await new AwsIamAdapter(client).create({ + values: {kind: 'roles', name: 'deployer', assumeRolePolicyDocument: '{"Version":"2012-10-17"}'}, + }) + + const command = sent[0] as CreateRoleCommand + expect(command).toBeInstanceOf(CreateRoleCommand) + expect(command.input.AssumeRolePolicyDocument).toBe('{"Version":"2012-10-17"}') + }) + + test('creates a policy with its document', async () => { + const {client, sent} = runtimeStub() + await new AwsIamAdapter(client).create({ + values: {kind: 'policies', name: 'read-buckets', policyDocument: '{"Statement":[]}'}, + }) + + expect((sent[0] as CreatePolicyCommand).input.PolicyDocument).toBe('{"Statement":[]}') + }) + + test('requires the document each kind cannot be created without', async () => { + // A role with no trust policy and a policy with no document are both + // rejected by IAM, so they are rejected here with a message that says + // which field is missing for the chosen kind. + const {client} = runtimeStub() + const adapter = new AwsIamAdapter(client) + + await expect(adapter.create({values: {kind: 'roles', name: 'r'}})).rejects.toThrow( + new ValidationError('assumeRolePolicyDocument is required when kind is roles'), + ) + await expect(adapter.create({values: {kind: 'policies', name: 'p'}})).rejects.toThrow( + new ValidationError('policyDocument is required when kind is policies'), + ) + }) + + test('rejects a document that is not JSON', async () => { + const {client} = runtimeStub() + + await expect( + new AwsIamAdapter(client).create({values: {kind: 'policies', name: 'p', policyDocument: 'not json'}}), + ).rejects.toThrow(ValidationError) + }) + + test('requires kind and name', async () => { + const {client} = runtimeStub() + const adapter = new AwsIamAdapter(client) + + await expect(adapter.create({values: {}})).rejects.toThrow(new ValidationError('kind is required')) + await expect(adapter.create({values: {kind: 'users'}})).rejects.toThrow( + new ValidationError('name is required'), + ) + }) + + describe('delete dispatches on the kind in the id', () => { + const cases: Array<[string, unknown]> = [ + ['user/alice', DeleteUserCommand], + ['role/deployer', DeleteRoleCommand], + ['policy/arn:aws:iam::000000000000:policy/read-buckets', DeletePolicyCommand], + ] + + for (const [id, command] of cases) { + test(id.split('/')[0], async () => { + const {client, sent} = runtimeStub() + await new AwsIamAdapter(client).delete(id) + + expect(sent[0]).toBeInstanceOf(command as never) + }) + } + }) +}) diff --git a/packages/api/src/adapter-aws/AwsIamAdapter.ts b/packages/api/src/adapter-aws/AwsIamAdapter.ts new file mode 100644 index 0000000..ffd7a2a --- /dev/null +++ b/packages/api/src/adapter-aws/AwsIamAdapter.ts @@ -0,0 +1,356 @@ +import { + CreatePolicyCommand, + CreateRoleCommand, + CreateUserCommand, + DeletePolicyCommand, + DeleteRoleCommand, + DeleteUserCommand, + GetPolicyCommand, + GetRoleCommand, + GetUserCommand, + type IAMClient, + ListPoliciesCommand, + ListRolesCommand, + ListUsersCommand, + type Policy, + type Role, + type User, +} from '@aws-sdk/client-iam' +import {RuntimeError, ValidationError} from '../cloud-spi/errors' +import {awsIamSchema, IAM_KIND_SINGULAR, IAM_KINDS, type IamKind} from '../cloud-spi/iamSchema' +import type { + CloudResource, + CloudServiceAdapter, + CreateResourceInput, + ResourceQuery, + ServiceSchema, +} from '../cloud-spi/types' + +/** + * IAM holds three kinds of resource under one service, which shapes the whole + * adapter: + * + * - `list` honours the `kind` facet from `ResourceQuery.filters`. With no facet + * it lists users, roles and policies together, which is what an audit view + * wants; with one it issues a single List call. + * - Resource ids are `{kind}/{identifier}` — `user/alice`, `role/deployer`, + * `policy/{arn}`. A bare name would be ambiguous across kinds and would not + * tell `get` or `delete` which API to call. Policies are keyed by ARN because + * that is what GetPolicy and DeletePolicy take. The ids survive the generic + * route because HttpClient encodes path params. + * - Policies are listed with `Scope: 'Local'`. Real IAM otherwise returns close + * to a thousand AWS-managed policies and buries the account's own. + */ +type Identity = + | {kind: 'users'; value: User} + | {kind: 'roles'; value: Role} + | {kind: 'policies'; value: Policy} + +const SINGULAR_TO_KIND: Record = {user: 'users', role: 'roles', policy: 'policies'} + +export class AwsIamAdapter implements CloudServiceAdapter { + readonly cloud = 'aws' as const + readonly service = 'iam' as const + + constructor(private readonly iam: IAMClient) {} + + schema(): ServiceSchema { + return awsIamSchema() + } + + async list(query: ResourceQuery = {}): Promise { + const kind = optionalKind(query.filters?.kind) + const kinds = kind ? [kind] : [...IAM_KINDS] + + const groups = await Promise.all(kinds.map((each) => this.listKind(each))) + return filterBySearch(groups.flat().map(toResource), query.search) + } + + async get(id: string): Promise { + const {kind, identifier} = parseId(id) + + try { + if (kind === 'users') { + const res = await this.iam.send(new GetUserCommand({UserName: identifier})) + return res.User ? toResource({kind, value: res.User}) : null + } + if (kind === 'roles') { + const res = await this.iam.send(new GetRoleCommand({RoleName: identifier})) + return res.Role ? toResource({kind, value: res.Role}) : null + } + const res = await this.iam.send(new GetPolicyCommand({PolicyArn: identifier})) + return res.Policy ? toResource({kind, value: res.Policy}) : null + } catch (error) { + if (isNoSuchEntity(error)) return null + throw error + } + } + + async create(input: CreateResourceInput): Promise { + const kind = requiredKind(input.values.kind) + const name = requiredString(input.values.name, 'name') + const path = optionalString(input.values.path, 'path') + + if (kind === 'users') { + const res = await this.iam.send(new CreateUserCommand({UserName: name, ...(path ? {Path: path} : {})})) + return this.created(res.User ? {kind, value: res.User} : null, name) + } + + if (kind === 'roles') { + const document = requiredJson( + input.values.assumeRolePolicyDocument, + 'assumeRolePolicyDocument', + 'roles', + ) + const res = await this.iam.send( + new CreateRoleCommand({ + RoleName: name, + AssumeRolePolicyDocument: document, + ...(path ? {Path: path} : {}), + }), + ) + return this.created(res.Role ? {kind, value: res.Role} : null, name) + } + + const document = requiredJson(input.values.policyDocument, 'policyDocument', 'policies') + const res = await this.iam.send( + new CreatePolicyCommand({PolicyName: name, PolicyDocument: document, ...(path ? {Path: path} : {})}), + ) + return this.created(res.Policy ? {kind, value: res.Policy} : null, name) + } + + async delete(id: string): Promise { + const {kind, identifier} = parseId(id) + + if (kind === 'users') { + await this.iam.send(new DeleteUserCommand({UserName: identifier})) + return + } + if (kind === 'roles') { + await this.iam.send(new DeleteRoleCommand({RoleName: identifier})) + return + } + await this.iam.send(new DeletePolicyCommand({PolicyArn: identifier})) + } + + private created(identity: Identity | null, name: string): CloudResource { + if (!identity) throw new RuntimeError(`IAM did not return the created resource for ${name}`) + return toResource(identity) + } + + /** Every List call pages on IsTruncated/Marker. */ + private async listKind(kind: IamKind): Promise { + const identities: Identity[] = [] + let marker: string | undefined + + do { + const page = await this.sendList(kind, marker) + identities.push(...page.identities) + marker = page.marker + } while (marker) + + return identities + } + + private async sendList( + kind: IamKind, + marker: string | undefined, + ): Promise<{identities: Identity[]; marker?: string}> { + const paging = marker ? {Marker: marker} : {} + + if (kind === 'users') { + const res = await this.iam.send(new ListUsersCommand(paging)) + return { + identities: (res.Users ?? []).map((value) => ({kind, value})), + marker: res.IsTruncated ? res.Marker : undefined, + } + } + if (kind === 'roles') { + const res = await this.iam.send(new ListRolesCommand(paging)) + return { + identities: (res.Roles ?? []).map((value) => ({kind, value})), + marker: res.IsTruncated ? res.Marker : undefined, + } + } + const res = await this.iam.send(new ListPoliciesCommand({Scope: 'Local', ...paging})) + return { + identities: (res.Policies ?? []).map((value) => ({kind, value})), + marker: res.IsTruncated ? res.Marker : undefined, + } + } +} + +function toResource(identity: Identity): CloudResource { + const singular = IAM_KIND_SINGULAR[identity.kind] + const common = { + cloud: 'aws' as const, + service: 'iam' as const, + region: null, + status: null, + } + + if (identity.kind === 'users') { + const user = identity.value + const name = user.UserName ?? '' + return { + ...common, + id: `${singular}/${name}`, + name, + type: 'iam-user', + createdAt: isoDate(user.CreateDate), + metadata: { + kind: singular, + arn: user.Arn, + path: user.Path, + userId: user.UserId, + passwordLastUsed: isoDate(user.PasswordLastUsed), + tags: mapTags(user.Tags), + }, + } + } + + if (identity.kind === 'roles') { + const role = identity.value + const name = role.RoleName ?? '' + return { + ...common, + id: `${singular}/${name}`, + name, + type: 'iam-role', + createdAt: isoDate(role.CreateDate), + metadata: { + kind: singular, + arn: role.Arn, + path: role.Path, + roleId: role.RoleId, + description: role.Description, + maxSessionDuration: role.MaxSessionDuration, + // IAM returns this percent-encoded; raw, it is unreadable in the + // inspector. + assumeRolePolicyDocument: decodePolicyDocument(role.AssumeRolePolicyDocument), + tags: mapTags(role.Tags), + }, + } + } + + const policy = identity.value + const name = policy.PolicyName ?? '' + return { + ...common, + // Keyed by ARN: GetPolicy and DeletePolicy take an ARN, not a name. + id: `${singular}/${policy.Arn ?? name}`, + name, + type: 'iam-policy', + createdAt: isoDate(policy.CreateDate), + metadata: { + kind: singular, + arn: policy.Arn, + path: policy.Path, + policyId: policy.PolicyId, + description: policy.Description, + attachmentCount: policy.AttachmentCount, + defaultVersionId: policy.DefaultVersionId, + updatedAt: isoDate(policy.UpdateDate), + tags: mapTags(policy.Tags), + }, + } +} + +/** `user/alice`, `role/deployer`, `policy/arn:aws:iam::...:policy/read-buckets`. */ +function parseId(id: string): {kind: IamKind; identifier: string} { + const separator = id.indexOf('/') + if (separator <= 0) { + throw new ValidationError(`IAM id must be "kind/name", for example user/alice, got "${id}"`) + } + + const prefix = id.slice(0, separator) + const identifier = id.slice(separator + 1) + const kind = SINGULAR_TO_KIND[prefix] + + if (!kind || !identifier) { + throw new ValidationError( + `IAM id must start with ${Object.keys(SINGULAR_TO_KIND).join(', ')}, got "${prefix}"`, + ) + } + return {kind, identifier} +} + +function optionalKind(value: unknown): IamKind | undefined { + if (value === undefined || value === null || value === '') return undefined + return requiredKind(value) +} + +function requiredKind(value: unknown): IamKind { + const raw = requiredString(value, 'kind') + if (!(IAM_KINDS as readonly string[]).includes(raw)) { + throw new ValidationError(`kind must be one of ${IAM_KINDS.join(', ')}`) + } + return raw as IamKind +} + +function filterBySearch(resources: CloudResource[], search?: string): CloudResource[] { + const normalized = search?.trim().toLowerCase() + if (!normalized) return resources + + return resources.filter((resource) => { + const arn = String(resource.metadata.arn ?? '').toLowerCase() + return ( + resource.name.toLowerCase().includes(normalized) || + resource.id.toLowerCase().includes(normalized) || + arn.includes(normalized) + ) + }) +} + +function decodePolicyDocument(document: string | undefined): string | undefined { + if (!document) return undefined + try { + return decodeURIComponent(document) + } catch { + // A document that is not percent-encoded is returned as-is rather than lost. + return document + } +} + +function mapTags(tags: Array<{Key?: string; Value?: string}> | undefined): Array<{key: string; value: string}> { + return (tags ?? []).map((tag) => ({key: tag.Key ?? '', value: tag.Value ?? ''})) +} + +function isoDate(date: Date | undefined): string | null { + return date ? date.toISOString() : null +} + +function requiredString(value: unknown, field: string): string { + const raw = optionalString(value, field) + if (raw === undefined) throw new ValidationError(`${field} is required`) + return raw +} + +function optionalString(value: unknown, field: string): string | undefined { + if (value === undefined || value === null || value === '') return undefined + if (typeof value !== 'string') throw new ValidationError(`${field} must be a string`) + return value.trim() || undefined +} + +/** + * IAM rejects a role without a trust policy and a policy without a document, so + * the message names the field *and* the kind that requires it — the form shows + * both fields for every kind. + */ +function requiredJson(value: unknown, field: string, kind: IamKind): string { + const raw = optionalString(value, field) + if (raw === undefined) throw new ValidationError(`${field} is required when kind is ${kind}`) + + try { + JSON.parse(raw) + } catch { + throw new ValidationError(`${field} must be valid JSON`) + } + return raw +} + +function isNoSuchEntity(error: unknown): boolean { + if (typeof error !== 'object' || error === null) return false + const name = (error as {name?: string}).name + return name === 'NoSuchEntity' || name === 'NoSuchEntityException' +} diff --git a/packages/api/src/aws.ts b/packages/api/src/aws.ts index e9ae0c9..0e8a4ca 100644 --- a/packages/api/src/aws.ts +++ b/packages/api/src/aws.ts @@ -4,6 +4,7 @@ import { EKSClient } from "@aws-sdk/client-eks"; import { EC2Client } from "@aws-sdk/client-ec2"; import { RDSClient } from "@aws-sdk/client-rds"; import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import { IAMClient } from "@aws-sdk/client-iam"; const endpoint = process.env.FLOCI_ENDPOINT; const region = process.env.AWS_REGION || "us-east-1"; @@ -39,6 +40,7 @@ export type AwsClients = { ec2: EC2Client; rds: RDSClient; secretsManager: SecretsManagerClient; + iam: IAMClient; }; export type AwsClientName = keyof AwsClients; @@ -58,6 +60,7 @@ function buildClients(accountId: string): AwsClients { ec2: new EC2Client(base), rds: new RDSClient(base), secretsManager: new SecretsManagerClient(base), + iam: new IAMClient(base), }; } diff --git a/packages/api/src/cloud-spi/iamSchema.ts b/packages/api/src/cloud-spi/iamSchema.ts new file mode 100644 index 0000000..a9b56c0 --- /dev/null +++ b/packages/api/src/cloud-spi/iamSchema.ts @@ -0,0 +1,103 @@ +import type {CloudProvider, FieldSchema, ServiceSchema, TableColumnSchema} from './types' + +/** + * IAM holds three different kinds of resource in one category, so the table needs + * a facet rather than only free-text search: "show me the roles" is not something + * a search box can express. + */ +export const IAM_KINDS = ['users', 'roles', 'policies'] as const +export type IamKind = (typeof IAM_KINDS)[number] + +/** Singular form used for a resource id prefix and the `type` field. */ +export const IAM_KIND_SINGULAR = {users: 'user', roles: 'role', policies: 'policy'} as const + +const iamColumns: TableColumnSchema[] = [ + {name: 'name', label: 'Name'}, + {name: 'type', label: 'Kind', format: 'badge'}, + {name: 'path', label: 'Path', path: 'metadata.path', emptyText: '/'}, + {name: 'arn', label: 'ARN', path: 'metadata.arn', format: 'code'}, + {name: 'createdAt', label: 'Created', format: 'datetime'}, +] + +const iamFilters: FieldSchema[] = [ + {name: 'search', label: 'Search', type: 'text', required: false}, + { + name: 'kind', + label: 'Kind', + type: 'select', + required: false, + description: 'Leave unset to list users, roles and policies together.', + options: IAM_KINDS.map((value) => ({label: value, value})), + }, +] + +export function awsIamSchema(): ServiceSchema { + return { + cloud: 'aws', + service: 'iam', + displayName: 'AWS IAM', + fields: [ + { + name: 'kind', + label: 'Kind', + type: 'select', + required: true, + group: 'Required', + options: IAM_KINDS.map((value) => ({label: value, value})), + }, + { + name: 'name', + label: 'Name', + type: 'text', + required: true, + group: 'Required', + validation: { + pattern: '^[\\w+=,.@-]+$', + minLength: 1, + maxLength: 128, + message: 'Letters, digits and + = , . @ _ - only.', + }, + }, + { + name: 'path', + label: 'Path', + type: 'text', + required: false, + description: 'Defaults to /.', + }, + { + name: 'assumeRolePolicyDocument', + label: 'Trust Policy (JSON)', + type: 'text', + required: false, + span: true, + group: 'Roles only', + description: 'Required when kind is roles. The trust policy that says who may assume the role.', + }, + { + name: 'policyDocument', + label: 'Policy Document (JSON)', + type: 'text', + required: false, + span: true, + group: 'Policies only', + description: 'Required when kind is policies.', + }, + ], + actions: ['list', 'create', 'delete', 'inspect'], + capabilities: { + resourceActions: [ + {name: 'list', label: 'List identities', enabled: true, status: 'available', runtimeRequired: true}, + {name: 'inspect', label: 'Inspect', enabled: true, status: 'available', runtimeRequired: true}, + {name: 'create', label: 'Create', enabled: true, status: 'available', runtimeRequired: true}, + {name: 'delete', label: 'Delete', enabled: true, status: 'available', runtimeRequired: true}, + ], + }, + filters: iamFilters, + columns: iamColumns, + } +} + +export function iamSchemaFor(cloud: CloudProvider): ServiceSchema | null { + return cloud === 'aws' ? awsIamSchema() : null +} diff --git a/packages/api/src/cloud-spi/serviceCatalog.ts b/packages/api/src/cloud-spi/serviceCatalog.ts index 285e910..ec86526 100644 --- a/packages/api/src/cloud-spi/serviceCatalog.ts +++ b/packages/api/src/cloud-spi/serviceCatalog.ts @@ -71,6 +71,12 @@ export const SERVICE_CATALOG = { storage: {displayName: 'Storage', iconKey: 'storage', group: 'Storage', order: 10}, database: {displayName: 'Database', iconKey: 'database', group: 'Databases', order: 10}, networking: {displayName: 'Networking', iconKey: 'networking', group: 'Networking', order: 10}, + iam: { + displayName: 'IAM', + iconKey: 'iam', + group: 'Security', + order: 40, + }, secrets: { displayName: 'Secrets Manager', displayNameByCloud: {azure: 'Key Vault'}, diff --git a/packages/api/src/cloudProxy.ts b/packages/api/src/cloudProxy.ts index 6240686..3f20f35 100644 --- a/packages/api/src/cloudProxy.ts +++ b/packages/api/src/cloudProxy.ts @@ -14,6 +14,7 @@ import {CloudProxyService} from './service/CloudProxyService' import {AzureServerlessAdapter} from './adapter-azure/AzureServerlessAdapter' import {AzureKeyVaultAdapter} from './adapter-azure/AzureKeyVaultAdapter' import {AwsServerlessAdapter} from './adapter-aws/AwsServerlessAdapter' +import {AwsIamAdapter} from './adapter-aws/AwsIamAdapter' import {awsClientsForAccount, resolveAccountId} from './aws' import {createEc2Service} from './services/ec2' import {createEksService} from './services/eks' @@ -39,6 +40,7 @@ export function createCloudAdapterRegistry(accountId?: string | null): CloudAdap new AwsComputeAdapter(ec2Service), new AwsNetworkingAdapter(ec2Service), new AwsServerlessAdapter(clients.lambda), + new AwsIamAdapter(clients.iam), new AzureStorageAdapter(), new AzureDatabaseAdapter(), new GcpStorageAdapter(), From 7599e8e81f059d52719ee5f01f28567109f275f5 Mon Sep 17 00:00:00 2001 From: TheSaifZaman Date: Tue, 28 Jul 2026 16:14:09 +0600 Subject: [PATCH 4/6] fix(iam): show a policy's document when inspecting it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A role's inspector surfaced its decoded trust policy but a policy showed only metadata, so the document being audited — the reason to open a policy at all — was never visible. get() now loads the default version with GetPolicyVersion and exposes the decoded document. Only on inspect: doing it during list would issue one extra call per row, which a test pins. The decode handles both shapes. Real IAM percent-encodes the document; the local runtime returns it literal, and decodeURIComponent leaves a string with no escapes unchanged, so neither is corrupted. A failed version lookup degrades the row with metadata.policyDocumentUnavailable rather than failing the inspect, since the rest of the policy metadata is still worth showing. --- .../api/src/adapter-aws/AwsIamAdapter.test.ts | 61 +++++++++++++++++++ packages/api/src/adapter-aws/AwsIamAdapter.ts | 37 ++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/packages/api/src/adapter-aws/AwsIamAdapter.test.ts b/packages/api/src/adapter-aws/AwsIamAdapter.test.ts index d896b3c..651d33f 100644 --- a/packages/api/src/adapter-aws/AwsIamAdapter.test.ts +++ b/packages/api/src/adapter-aws/AwsIamAdapter.test.ts @@ -7,6 +7,7 @@ import { DeleteRoleCommand, DeleteUserCommand, GetPolicyCommand, + GetPolicyVersionCommand, GetRoleCommand, GetUserCommand, type IAMClient, @@ -196,6 +197,66 @@ describe('AwsIamAdapter', () => { } }) + describe('inspecting a policy loads its document', () => { + const POLICY_ID = `policy/${policy.Arn}` + + test('fetches the default version and exposes the decoded document', async () => { + // Roles surface a decoded trust policy, so a policy has to surface the + // document it was created with — that is the thing being audited. + const {client, sent} = stubIam((command) => { + if (command instanceof GetPolicyCommand) return {Policy: policy} + if (command instanceof GetPolicyVersionCommand) { + return {PolicyVersion: {VersionId: 'v1', Document: '%7B%22Statement%22%3A%5B%5D%7D'}} + } + return {} + }) + const resource = await new AwsIamAdapter(client).get(POLICY_ID) + + const versionCall = sent.find((c) => c instanceof GetPolicyVersionCommand) as GetPolicyVersionCommand + expect(versionCall.input.PolicyArn).toBe(policy.Arn) + expect(versionCall.input.VersionId).toBe('v1') + expect(resource?.metadata.policyDocument).toBe('{"Statement":[]}') + }) + + test('leaves a document the runtime did not encode untouched', async () => { + // Real IAM percent-encodes the document; the local runtime returns it + // literal, so decoding must be a no-op rather than a corruption. + const {client} = stubIam((command) => { + if (command instanceof GetPolicyCommand) return {Policy: policy} + if (command instanceof GetPolicyVersionCommand) { + return {PolicyVersion: {VersionId: 'v1', Document: '{"Statement":[]}'}} + } + return {} + }) + const resource = await new AwsIamAdapter(client).get(POLICY_ID) + + expect(resource?.metadata.policyDocument).toBe('{"Statement":[]}') + }) + + test('still inspects the policy when the version lookup fails', async () => { + const {client} = stubIam((command) => { + if (command instanceof GetPolicyCommand) return {Policy: policy} + if (command instanceof GetPolicyVersionCommand) { + throw Object.assign(new Error('Rate exceeded'), {name: 'ThrottlingException'}) + } + return {} + }) + const resource = await new AwsIamAdapter(client).get(POLICY_ID) + + expect(resource?.name).toBe('read-buckets') + expect(resource?.metadata.policyDocument).toBeUndefined() + expect(resource?.metadata.policyDocumentUnavailable).toBe(true) + }) + + test('does not fetch the document while listing', async () => { + // One GetPolicyVersion per row would turn a list into an N+1. + const {client, sent} = runtimeStub() + await new AwsIamAdapter(client).list({filters: {kind: 'policies'}}) + + expect(sent.some((c) => c instanceof GetPolicyVersionCommand)).toBe(false) + }) + }) + test('rejects an id that does not name a kind', async () => { const {client} = runtimeStub() diff --git a/packages/api/src/adapter-aws/AwsIamAdapter.ts b/packages/api/src/adapter-aws/AwsIamAdapter.ts index ffd7a2a..d67db89 100644 --- a/packages/api/src/adapter-aws/AwsIamAdapter.ts +++ b/packages/api/src/adapter-aws/AwsIamAdapter.ts @@ -6,6 +6,7 @@ import { DeleteRoleCommand, DeleteUserCommand, GetPolicyCommand, + GetPolicyVersionCommand, GetRoleCommand, GetUserCommand, type IAMClient, @@ -48,6 +49,9 @@ type Identity = const SINGULAR_TO_KIND: Record = {user: 'users', role: 'roles', policy: 'policies'} +/** A policy's document, or the fact that it could not be read. */ +type DocumentLookup = {document?: string; unavailable?: true} + export class AwsIamAdapter implements CloudServiceAdapter { readonly cloud = 'aws' as const readonly service = 'iam' as const @@ -63,7 +67,11 @@ export class AwsIamAdapter implements CloudServiceAdapter { const kinds = kind ? [kind] : [...IAM_KINDS] const groups = await Promise.all(kinds.map((each) => this.listKind(each))) - return filterBySearch(groups.flat().map(toResource), query.search) + // Not `.map(toResource)`: map would pass the index as the document argument. + return filterBySearch( + groups.flat().map((identity) => toResource(identity)), + query.search, + ) } async get(id: string): Promise { @@ -79,7 +87,9 @@ export class AwsIamAdapter implements CloudServiceAdapter { return res.Role ? toResource({kind, value: res.Role}) : null } const res = await this.iam.send(new GetPolicyCommand({PolicyArn: identifier})) - return res.Policy ? toResource({kind, value: res.Policy}) : null + if (!res.Policy) return null + // Only on inspect: one GetPolicyVersion per row would make list an N+1. + return toResource({kind, value: res.Policy}, await this.policyDocument(res.Policy)) } catch (error) { if (isNoSuchEntity(error)) return null throw error @@ -133,6 +143,24 @@ export class AwsIamAdapter implements CloudServiceAdapter { await this.iam.send(new DeletePolicyCommand({PolicyArn: identifier})) } + /** + * Load the policy's default version so inspect can show the document, the way + * a role shows its trust policy. Failure degrades the row rather than the + * request — the policy metadata is worth showing without the body. + */ + private async policyDocument(policy: Policy): Promise { + const arn = policy.Arn + const versionId = policy.DefaultVersionId + if (!arn || !versionId) return {} + + try { + const res = await this.iam.send(new GetPolicyVersionCommand({PolicyArn: arn, VersionId: versionId})) + return {document: decodePolicyDocument(res.PolicyVersion?.Document)} + } catch { + return {unavailable: true} + } + } + private created(identity: Identity | null, name: string): CloudResource { if (!identity) throw new RuntimeError(`IAM did not return the created resource for ${name}`) return toResource(identity) @@ -180,7 +208,7 @@ export class AwsIamAdapter implements CloudServiceAdapter { } } -function toResource(identity: Identity): CloudResource { +function toResource(identity: Identity, policyDoc: DocumentLookup = {}): CloudResource { const singular = IAM_KIND_SINGULAR[identity.kind] const common = { cloud: 'aws' as const, @@ -250,6 +278,9 @@ function toResource(identity: Identity): CloudResource { description: policy.Description, attachmentCount: policy.AttachmentCount, defaultVersionId: policy.DefaultVersionId, + /** Only populated by inspect; list would be an N+1. */ + ...(policyDoc.document ? {policyDocument: policyDoc.document} : {}), + ...(policyDoc.unavailable ? {policyDocumentUnavailable: true} : {}), updatedAt: isoDate(policy.UpdateDate), tags: mapTags(policy.Tags), }, From 5911b5ac24baeb13be30bb6cf6faa6db2303155f Mon Sep 17 00:00:00 2001 From: TheSaifZaman Date: Tue, 28 Jul 2026 20:22:07 +0600 Subject: [PATCH 5/6] fix(frontend): mirror the IAM resource types into the resource type union MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frontend keeps its own copy of CloudResource.type — the packages are independent and share no dependency — so widening the API union without widening this one leaves the frontend contract behind the payloads the API actually returns. KnownResourceType | (string & {}). This branch is off main, where the union is still closed, so the member is added directly; if #156 merges first the one-line conflict resolves by moving it into KnownResourceType. --- README.md | 1 + packages/frontend/src/types/resource.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a7b58d2..4c470c6 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ cd packages/api && bun run scripts/service-matrix.ts | Databases | Database | Yes (list, inspect) | Yes (list, create, delete, inspect) | Yes (list, create, inspect, delete) | | Networking | Networking | Yes (list) | No | No | | Security | Secrets Manager / Key Vault | Yes (legacy page) | Yes (list, create, delete, inspect) | No | +| Security | IAM | Yes (list, create, delete, inspect) | No | No | Console Home is available for all three clouds. diff --git a/packages/frontend/src/types/resource.ts b/packages/frontend/src/types/resource.ts index a69e1df..85a1cdb 100644 --- a/packages/frontend/src/types/resource.ts +++ b/packages/frontend/src/types/resource.ts @@ -5,7 +5,7 @@ export interface CloudResource { name: string cloud: CloudProvider service: CloudServiceType - type: 'bucket' | 'container' | 'cluster' | 'db-instance' | 'cosmos-database' | 'instance' | 'image' | 'vpc' | 'lambda' | "azure-function" | 'gcp-function' | 'secret'; + type: 'bucket' | 'container' | 'cluster' | 'db-instance' | 'cosmos-database' | 'instance' | 'image' | 'vpc' | 'lambda' | "azure-function" | 'gcp-function' | 'secret' | 'iam-user' | 'iam-role' | 'iam-policy'; region: string | null createdAt: string | null status?: string | null From 1d33ed8aaead653ef94a1973b01fdad60b96b9c6 Mon Sep 17 00:00:00 2001 From: TheSaifZaman Date: Wed, 29 Jul 2026 09:39:51 +0600 Subject: [PATCH 6/6] docs(iam): state that the kind facet is API-only for now --- packages/api/src/cloud-spi/iamSchema.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/api/src/cloud-spi/iamSchema.ts b/packages/api/src/cloud-spi/iamSchema.ts index a9b56c0..61366cb 100644 --- a/packages/api/src/cloud-spi/iamSchema.ts +++ b/packages/api/src/cloud-spi/iamSchema.ts @@ -19,6 +19,16 @@ const iamColumns: TableColumnSchema[] = [ {name: 'createdAt', label: 'Created', format: 'datetime'}, ] +/** + * `kind` is **API-only today**, and that is a stated choice rather than a gap. + * + * `DynamicResourceView` sends only `search`, and nothing in the frontend renders + * `schema.filters` as controls, so this facet is reachable by calling the API + * directly and not from the console. Rendering non-search filters is a shared + * piece of frontend work — every category with a facet needs it — so it belongs + * in its own PR rather than half-built here, and it is deliberately not blocking + * the adapter. + */ const iamFilters: FieldSchema[] = [ {name: 'search', label: 'Search', type: 'text', required: false}, {