diff --git a/app/aws-lsp-codewhisperer-runtimes/package.json b/app/aws-lsp-codewhisperer-runtimes/package.json index 67e9cbb738..8997f866bd 100644 --- a/app/aws-lsp-codewhisperer-runtimes/package.json +++ b/app/aws-lsp-codewhisperer-runtimes/package.json @@ -22,7 +22,7 @@ "local-build": "node scripts/local-build.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.116", + "@aws/language-server-runtimes": "^0.2.118", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", diff --git a/package-lock.json b/package-lock.json index 7a703c9b43..e3378abaa3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,14 @@ "integration-tests/*" ], "dependencies": { - "@aws/language-server-runtimes": "^0.2.116", + "@aws/language-server-runtimes": "^0.2.118", "@smithy/types": "4.2.0", "clean": "^4.0.2", "typescript": "^5.8.2" }, "devDependencies": { + "@aws-sdk/client-iam": "^3.840.0", + "@aws-sdk/client-sts": "^3.840.0", "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", "@types/ignore-walk": "^4.0.3", @@ -87,7 +89,7 @@ "name": "@aws/lsp-codewhisperer-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.116", + "@aws/language-server-runtimes": "^0.2.118", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", @@ -1275,44 +1277,1092 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/client-iam": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-iam/-/client-iam-3.840.0.tgz", + "integrity": "sha512-+HWqpTwXQYhFzgwfjGFHfo+a0mRQwYq29BEYlgfcydo8UOApc1oxsVmEmnYh2nbukaefUkOaMDb1xORybsE6Lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/credential-provider-node": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.840.0.tgz", + "integrity": "sha512-3Zp+FWN2hhmKdpS0Ragi5V2ZPsZNScE3jlbgoJjzjI/roHZqO+e3/+XFN4TlM0DsPKYJNp+1TAjmhxN6rOnfYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/core": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.840.0.tgz", + "integrity": "sha512-x3Zgb39tF1h2XpU+yA4OAAQlW6LVEfXNlSedSYJ7HGKXqA/E9h3rWQVpYfhXXVVsLdYXdNw5KBUkoAoruoZSZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.6.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.840.0.tgz", + "integrity": "sha512-EzF6VcJK7XvQ/G15AVEfJzN2mNXU8fcVpXo4bRyr1S6t2q5zx6UPH/XjDbn18xyUmOq01t+r8gG+TmHEVo18fA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.840.0.tgz", + "integrity": "sha512-wbnUiPGLVea6mXbUh04fu+VJmGkQvmToPeTYdHE8eRZq3NRDi3t3WltT+jArLBKD/4NppRpMjf2ju4coMCz91g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.840.0.tgz", + "integrity": "sha512-7F290BsWydShHb+7InXd+IjJc3mlEIm9I0R57F/Pjl1xZB69MdkhVGCnuETWoBt4g53ktJd6NEjzm/iAhFXFmw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/credential-provider-env": "3.840.0", + "@aws-sdk/credential-provider-http": "3.840.0", + "@aws-sdk/credential-provider-process": "3.840.0", + "@aws-sdk/credential-provider-sso": "3.840.0", + "@aws-sdk/credential-provider-web-identity": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.840.0.tgz", + "integrity": "sha512-KufP8JnxA31wxklLm63evUPSFApGcH8X86z3mv9SRbpCm5ycgWIGVCTXpTOdgq6rPZrwT9pftzv2/b4mV/9clg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.840.0", + "@aws-sdk/credential-provider-http": "3.840.0", + "@aws-sdk/credential-provider-ini": "3.840.0", + "@aws-sdk/credential-provider-process": "3.840.0", + "@aws-sdk/credential-provider-sso": "3.840.0", + "@aws-sdk/credential-provider-web-identity": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.840.0.tgz", + "integrity": "sha512-HkDQWHy8tCI4A0Ps2NVtuVYMv9cB4y/IuD/TdOsqeRIAT12h8jDb98BwQPNLAImAOwOWzZJ8Cu0xtSpX7CQhMw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.840.0.tgz", + "integrity": "sha512-2qgdtdd6R0Z1y0KL8gzzwFUGmhBHSUx4zy85L2XV1CXhpRNwV71SVWJqLDVV5RVWVf9mg50Pm3AWrUC0xb0pcA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.840.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/token-providers": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.840.0.tgz", + "integrity": "sha512-dpEeVXG8uNZSmVXReE4WP0lwoioX2gstk4RnUgrdUE3YaPq8A+hJiVAyc3h+cjDeIqfbsQbZm9qFetKC2LF9dQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.840.0.tgz", + "integrity": "sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-logger": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.840.0.tgz", + "integrity": "sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.840.0.tgz", + "integrity": "sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.840.0.tgz", + "integrity": "sha512-hiiMf7BP5ZkAFAvWRcK67Mw/g55ar7OCrvrynC92hunx/xhMkrgSLM0EXIZ1oTn3uql9kH/qqGF0nqsK6K555A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@smithy/core": "^3.6.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/nested-clients": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.840.0.tgz", + "integrity": "sha512-LXYYo9+n4hRqnRSIMXLBb+BLz+cEmjMtTudwK1BF6Bn2RfdDv29KuyeDRrPCS3TwKl7ZKmXUmE9n5UuHAPfBpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.840.0.tgz", + "integrity": "sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/token-providers": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.840.0.tgz", + "integrity": "sha512-6BuTOLTXvmgwjK7ve7aTg9JaWFdM5UoMolLVPMyh3wTv9Ufalh8oklxYHUBIxsKkBGO2WiHXytveuxH6tAgTYg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/types": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.840.0.tgz", + "integrity": "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-endpoints": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.840.0.tgz", + "integrity": "sha512-eqE9ROdg/Kk0rj3poutyRCFauPDXIf/WSvCqFiRDDVi6QOnCv/M0g2XW8/jSvkJlOyaXkNCptapIp6BeeFFGYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "@smithy/util-endpoints": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.840.0.tgz", + "integrity": "sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.840.0.tgz", + "integrity": "sha512-Fy5JUEDQU1tPm2Yw/YqRYYc27W5+QD/J4mYvQvdWjUGZLB5q3eLFMGD35Uc28ZFoGMufPr4OCxK/bRfWROBRHQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-s3": { "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.839.0.tgz", - "integrity": "sha512-7zDInY+qltKxeG+9d/97nbs+FWINcAi5bChBrleUQkuQ/dA9pSP1URo/6JlVzD2Ejvksm+hVK6z3VUWZaIAVOw==", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.839.0.tgz", + "integrity": "sha512-7zDInY+qltKxeG+9d/97nbs+FWINcAi5bChBrleUQkuQ/dA9pSP1URo/6JlVzD2Ejvksm+hVK6z3VUWZaIAVOw==", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.839.0", + "@aws-sdk/credential-provider-node": "3.839.0", + "@aws-sdk/middleware-bucket-endpoint": "3.830.0", + "@aws-sdk/middleware-expect-continue": "3.821.0", + "@aws-sdk/middleware-flexible-checksums": "3.839.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-location-constraint": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-sdk-s3": "3.839.0", + "@aws-sdk/middleware-ssec": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.839.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/signature-v4-multi-region": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.839.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/eventstream-serde-browser": "^4.0.4", + "@smithy/eventstream-serde-config-resolver": "^4.1.2", + "@smithy/eventstream-serde-node": "^4.0.4", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-blob-browser": "^4.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/hash-stream-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/md5-js": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.6", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.839.0.tgz", + "integrity": "sha512-AZABysUhbfcwXVlMo97/vwHgsfJNF81wypCAowpqAJkSjP2KrqsqHpb71/RoR2w8JGmEnBBXRD4wIxDhnmifWg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.839.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.839.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.839.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.839.0.tgz", + "integrity": "sha512-KdwL5RaK7eUIlOpdOoZ5u+2t4X1rdX/MTZgz3IV/aBzjVUoGsp+uUnbyqXomLQSUitPHp72EE/NHDsvWW/IHvQ==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.6.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.839.0.tgz", + "integrity": "sha512-cWTadewPPz1OvObZJB+olrgh8VwcgIVcT293ZUT9V0CMF0UU7QaPwJP7uNXcNxltTh+sk1yhjH4UlcnJigZZbA==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.839.0.tgz", + "integrity": "sha512-fv0BZwrDhWDju4D1MCLT4I2aPjr0dVQ6P+MpqvcGNOA41Oa9UdRhYTV5iuy5NLXzIzoCmnS+XfSq5Kbsf6//xw==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.839.0.tgz", + "integrity": "sha512-GHm0hF4CiDxIDR7TauMaA6iI55uuSqRxMBcqTAHaTPm6+h1A+MS+ysQMxZ+Jvwtoy8WmfTIGrJVxSCw0sK2hvA==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/credential-provider-env": "3.839.0", + "@aws-sdk/credential-provider-http": "3.839.0", + "@aws-sdk/credential-provider-process": "3.839.0", + "@aws-sdk/credential-provider-sso": "3.839.0", + "@aws-sdk/credential-provider-web-identity": "3.839.0", + "@aws-sdk/nested-clients": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.839.0.tgz", + "integrity": "sha512-7bR+U2h+ft0V8chyeu9Bh/pvau4ZkQMeRt5f0dAULoepZQ77QQVRP4H04yJPTg9DCtqbVULQ3uf5YOp1/08vQw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.839.0", + "@aws-sdk/credential-provider-http": "3.839.0", + "@aws-sdk/credential-provider-ini": "3.839.0", + "@aws-sdk/credential-provider-process": "3.839.0", + "@aws-sdk/credential-provider-sso": "3.839.0", + "@aws-sdk/credential-provider-web-identity": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.839.0.tgz", + "integrity": "sha512-qShpekjociUZ+isyQNa0P7jo+0q3N2+0eJDg8SGyP6K6hHTcGfiqxTDps+IKl6NreCPhZCBzyI9mWkP0xSDR6g==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.839.0.tgz", + "integrity": "sha512-w10zBLHhU8SBQcdrSPMI02haLoRGZg+gP7mH/Er8VhIXfHefbr7o4NirmB0hwdw/YAH8MLlC9jj7c2SJlsNhYA==", + "dependencies": { + "@aws-sdk/client-sso": "3.839.0", + "@aws-sdk/core": "3.839.0", + "@aws-sdk/token-providers": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.839.0.tgz", + "integrity": "sha512-EvqTc7J1kgmiuxknpCp1S60hyMQvmKxsI5uXzQtcogl/N55rxiXEqnCLI5q6p33q91PJegrcMCM5Q17Afhm5qA==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/nested-clients": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", + "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", + "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", + "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.839.0.tgz", + "integrity": "sha512-2u74uRM1JWq6Sf7+3YpjejPM9YkomGt4kWhrmooIBEq1k5r2GTbkH7pNCxBQwBueXM21jAGVDxxeClpTx+5hig==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@smithy/core": "^3.6.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.839.0.tgz", + "integrity": "sha512-Glic0pg2THYP3aRhJORwJJBe1JLtJoEdWV/MFZNyzCklfMwEzpWtZAyxy+tQyFmMeW50uBAnh2R0jhMMcf257w==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.839.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.839.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.839.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", + "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", + "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.839.0.tgz", + "integrity": "sha512-MuunkIG1bJVMtTH7MbjXOrhHleU5wjHz5eCAUc6vj7M9rwol71nqjj9b8RLnkO5gsJcKc29Qk8iV6xQuzKWNMw==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.731.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.731.0.tgz", + "integrity": "sha512-O4C/UYGgqMsBg21MMApFdgyh8BX568hQhbdoNFmRVTBoSnCZ3w+H4a1wBPX4Gyl0NX+ab6Xxo9rId8HiyPXJ0A==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.731.0", + "@aws-sdk/middleware-host-header": "3.731.0", + "@aws-sdk/middleware-logger": "3.731.0", + "@aws-sdk/middleware-recursion-detection": "3.731.0", + "@aws-sdk/middleware-user-agent": "3.731.0", + "@aws-sdk/region-config-resolver": "3.731.0", + "@aws-sdk/types": "3.731.0", + "@aws-sdk/util-endpoints": "3.731.0", + "@aws-sdk/util-user-agent-browser": "3.731.0", + "@aws-sdk/util-user-agent-node": "3.731.0", + "@smithy/config-resolver": "^4.0.0", + "@smithy/core": "^3.0.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/hash-node": "^4.0.0", + "@smithy/invalid-dependency": "^4.0.0", + "@smithy/middleware-content-length": "^4.0.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/middleware-retry": "^4.0.0", + "@smithy/middleware-serde": "^4.0.0", + "@smithy/middleware-stack": "^4.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/url-parser": "^4.0.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.0", + "@smithy/util-defaults-mode-node": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "@smithy/util-middleware": "^4.0.0", + "@smithy/util-retry": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.839.0.tgz", + "integrity": "sha512-7QnMApYfQBT441YkxObxt1hZ8TdqZH7h0NdYsvbLdEqGROXBDDT+Wq7ZVfsnKjuVUGQ/t75bIqFn7M8cdyESfA==", "dependencies": { - "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.839.0", "@aws-sdk/credential-provider-node": "3.839.0", - "@aws-sdk/middleware-bucket-endpoint": "3.830.0", - "@aws-sdk/middleware-expect-continue": "3.821.0", - "@aws-sdk/middleware-flexible-checksums": "3.839.0", "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-location-constraint": "3.821.0", "@aws-sdk/middleware-logger": "3.821.0", "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-sdk-s3": "3.839.0", - "@aws-sdk/middleware-ssec": "3.821.0", "@aws-sdk/middleware-user-agent": "3.839.0", "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/signature-v4-multi-region": "3.839.0", "@aws-sdk/types": "3.821.0", "@aws-sdk/util-endpoints": "3.828.0", "@aws-sdk/util-user-agent-browser": "3.821.0", "@aws-sdk/util-user-agent-node": "3.839.0", - "@aws-sdk/xml-builder": "3.821.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.6.0", - "@smithy/eventstream-serde-browser": "^4.0.4", - "@smithy/eventstream-serde-config-resolver": "^4.1.2", - "@smithy/eventstream-serde-node": "^4.0.4", "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/hash-blob-browser": "^4.0.4", "@smithy/hash-node": "^4.0.4", - "@smithy/hash-stream-node": "^4.0.4", "@smithy/invalid-dependency": "^4.0.4", - "@smithy/md5-js": "^4.0.4", "@smithy/middleware-content-length": "^4.0.4", "@smithy/middleware-endpoint": "^4.1.13", "@smithy/middleware-retry": "^4.1.14", @@ -1332,18 +2382,14 @@ "@smithy/util-endpoints": "^3.0.6", "@smithy/util-middleware": "^4.0.4", "@smithy/util-retry": "^4.0.6", - "@smithy/util-stream": "^4.2.2", "@smithy/util-utf8": "^4.0.0", - "@smithy/util-waiter": "^4.0.6", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/client-sso": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.839.0.tgz", "integrity": "sha512-AZABysUhbfcwXVlMo97/vwHgsfJNF81wypCAowpqAJkSjP2KrqsqHpb71/RoR2w8JGmEnBBXRD4wIxDhnmifWg==", @@ -1391,7 +2437,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/core": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.839.0.tgz", "integrity": "sha512-KdwL5RaK7eUIlOpdOoZ5u+2t4X1rdX/MTZgz3IV/aBzjVUoGsp+uUnbyqXomLQSUitPHp72EE/NHDsvWW/IHvQ==", @@ -1416,7 +2462,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-env": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.839.0.tgz", "integrity": "sha512-cWTadewPPz1OvObZJB+olrgh8VwcgIVcT293ZUT9V0CMF0UU7QaPwJP7uNXcNxltTh+sk1yhjH4UlcnJigZZbA==", @@ -1431,7 +2477,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-http": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.839.0.tgz", "integrity": "sha512-fv0BZwrDhWDju4D1MCLT4I2aPjr0dVQ6P+MpqvcGNOA41Oa9UdRhYTV5iuy5NLXzIzoCmnS+XfSq5Kbsf6//xw==", @@ -1451,7 +2497,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-ini": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.839.0.tgz", "integrity": "sha512-GHm0hF4CiDxIDR7TauMaA6iI55uuSqRxMBcqTAHaTPm6+h1A+MS+ysQMxZ+Jvwtoy8WmfTIGrJVxSCw0sK2hvA==", @@ -1474,7 +2520,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-node": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.839.0.tgz", "integrity": "sha512-7bR+U2h+ft0V8chyeu9Bh/pvau4ZkQMeRt5f0dAULoepZQ77QQVRP4H04yJPTg9DCtqbVULQ3uf5YOp1/08vQw==", @@ -1496,7 +2542,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-process": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.839.0.tgz", "integrity": "sha512-qShpekjociUZ+isyQNa0P7jo+0q3N2+0eJDg8SGyP6K6hHTcGfiqxTDps+IKl6NreCPhZCBzyI9mWkP0xSDR6g==", @@ -1512,7 +2558,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-sso": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.839.0.tgz", "integrity": "sha512-w10zBLHhU8SBQcdrSPMI02haLoRGZg+gP7mH/Er8VhIXfHefbr7o4NirmB0hwdw/YAH8MLlC9jj7c2SJlsNhYA==", @@ -1530,7 +2576,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.839.0.tgz", "integrity": "sha512-EvqTc7J1kgmiuxknpCp1S60hyMQvmKxsI5uXzQtcogl/N55rxiXEqnCLI5q6p33q91PJegrcMCM5Q17Afhm5qA==", @@ -1546,7 +2592,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-host-header": { "version": "3.821.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", @@ -1560,7 +2606,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-logger": { "version": "3.821.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", @@ -1573,7 +2619,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.821.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", @@ -1587,7 +2633,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.839.0.tgz", "integrity": "sha512-2u74uRM1JWq6Sf7+3YpjejPM9YkomGt4kWhrmooIBEq1k5r2GTbkH7pNCxBQwBueXM21jAGVDxxeClpTx+5hig==", @@ -1604,7 +2650,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/nested-clients": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.839.0.tgz", "integrity": "sha512-Glic0pg2THYP3aRhJORwJJBe1JLtJoEdWV/MFZNyzCklfMwEzpWtZAyxy+tQyFmMeW50uBAnh2R0jhMMcf257w==", @@ -1652,7 +2698,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/region-config-resolver": { "version": "3.821.0", "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", @@ -1668,7 +2714,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.821.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", @@ -1679,7 +2725,7 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.839.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.839.0.tgz", "integrity": "sha512-MuunkIG1bJVMtTH7MbjXOrhHleU5wjHz5eCAUc6vj7M9rwol71nqjj9b8RLnkO5gsJcKc29Qk8iV6xQuzKWNMw==", @@ -1702,7 +2748,7 @@ } } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/abort-controller": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/abort-controller": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", @@ -1714,7 +2760,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/node-http-handler": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/node-http-handler": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", @@ -1729,7 +2775,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/types": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/types": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", @@ -1740,84 +2786,90 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" + "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { + "version": "3.731.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.731.0.tgz", + "integrity": "sha512-NrdkJg6oOUbXR2r9WvHP408CLyvST8cJfp1/jP9pemtjvjPoh6NukbCtiSFdOOb1eryP02CnqQWItfJC1p2Y/Q==", + "dependencies": { + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso": { + "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { "version": "3.731.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.731.0.tgz", - "integrity": "sha512-O4C/UYGgqMsBg21MMApFdgyh8BX568hQhbdoNFmRVTBoSnCZ3w+H4a1wBPX4Gyl0NX+ab6Xxo9rId8HiyPXJ0A==", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.731.0.tgz", + "integrity": "sha512-riztxTAfncFS9yQWcBJffGgOgLoKSa63ph+rxWJxKl6BHAmWEvHICj1qDcVmnWfIcvJ5cClclY75l9qKaUH7rQ==", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.731.0", - "@aws-sdk/middleware-host-header": "3.731.0", - "@aws-sdk/middleware-logger": "3.731.0", - "@aws-sdk/middleware-recursion-detection": "3.731.0", - "@aws-sdk/middleware-user-agent": "3.731.0", - "@aws-sdk/region-config-resolver": "3.731.0", "@aws-sdk/types": "3.731.0", - "@aws-sdk/util-endpoints": "3.731.0", - "@aws-sdk/util-user-agent-browser": "3.731.0", - "@aws-sdk/util-user-agent-node": "3.731.0", - "@smithy/config-resolver": "^4.0.0", - "@smithy/core": "^3.0.0", - "@smithy/fetch-http-handler": "^5.0.0", - "@smithy/hash-node": "^4.0.0", - "@smithy/invalid-dependency": "^4.0.0", - "@smithy/middleware-content-length": "^4.0.0", - "@smithy/middleware-endpoint": "^4.0.0", - "@smithy/middleware-retry": "^4.0.0", - "@smithy/middleware-serde": "^4.0.0", - "@smithy/middleware-stack": "^4.0.0", - "@smithy/node-config-provider": "^4.0.0", - "@smithy/node-http-handler": "^4.0.0", - "@smithy/protocol-http": "^5.0.0", - "@smithy/smithy-client": "^4.0.0", "@smithy/types": "^4.0.0", - "@smithy/url-parser": "^4.0.0", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.0", - "@smithy/util-defaults-mode-node": "^4.0.0", "@smithy/util-endpoints": "^3.0.0", - "@smithy/util-middleware": "^4.0.0", - "@smithy/util-retry": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.839.0.tgz", - "integrity": "sha512-7QnMApYfQBT441YkxObxt1hZ8TdqZH7h0NdYsvbLdEqGROXBDDT+Wq7ZVfsnKjuVUGQ/t75bIqFn7M8cdyESfA==", + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.840.0.tgz", + "integrity": "sha512-h+mu89Wk81Ne+B624GT/pBM5VjuAZueSeQNixhgtQ1QHi6bZzrpz8+lvMSibKO+kXFyQsTLzkyibbxnhLpWQZA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.839.0", - "@aws-sdk/credential-provider-node": "3.839.0", - "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-logger": "3.821.0", - "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-user-agent": "3.839.0", - "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@aws-sdk/util-user-agent-browser": "3.821.0", - "@aws-sdk/util-user-agent-node": "3.839.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/credential-provider-node": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.6.0", "@smithy/fetch-http-handler": "^5.0.4", @@ -1849,23 +2901,25 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/client-sso": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.839.0.tgz", - "integrity": "sha512-AZABysUhbfcwXVlMo97/vwHgsfJNF81wypCAowpqAJkSjP2KrqsqHpb71/RoR2w8JGmEnBBXRD4wIxDhnmifWg==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/client-sso": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.840.0.tgz", + "integrity": "sha512-3Zp+FWN2hhmKdpS0Ragi5V2ZPsZNScE3jlbgoJjzjI/roHZqO+e3/+XFN4TlM0DsPKYJNp+1TAjmhxN6rOnfYA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.839.0", - "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-logger": "3.821.0", - "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-user-agent": "3.839.0", - "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@aws-sdk/util-user-agent-browser": "3.821.0", - "@aws-sdk/util-user-agent-node": "3.839.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.6.0", "@smithy/fetch-http-handler": "^5.0.4", @@ -1897,12 +2951,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/core": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.839.0.tgz", - "integrity": "sha512-KdwL5RaK7eUIlOpdOoZ5u+2t4X1rdX/MTZgz3IV/aBzjVUoGsp+uUnbyqXomLQSUitPHp72EE/NHDsvWW/IHvQ==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/core": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.840.0.tgz", + "integrity": "sha512-x3Zgb39tF1h2XpU+yA4OAAQlW6LVEfXNlSedSYJ7HGKXqA/E9h3rWQVpYfhXXVVsLdYXdNw5KBUkoAoruoZSZA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@aws-sdk/xml-builder": "3.821.0", "@smithy/core": "^3.6.0", "@smithy/node-config-provider": "^4.1.3", @@ -1922,13 +2978,15 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.839.0.tgz", - "integrity": "sha512-cWTadewPPz1OvObZJB+olrgh8VwcgIVcT293ZUT9V0CMF0UU7QaPwJP7uNXcNxltTh+sk1yhjH4UlcnJigZZbA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.840.0.tgz", + "integrity": "sha512-EzF6VcJK7XvQ/G15AVEfJzN2mNXU8fcVpXo4bRyr1S6t2q5zx6UPH/XjDbn18xyUmOq01t+r8gG+TmHEVo18fA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -1937,13 +2995,15 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.839.0.tgz", - "integrity": "sha512-fv0BZwrDhWDju4D1MCLT4I2aPjr0dVQ6P+MpqvcGNOA41Oa9UdRhYTV5iuy5NLXzIzoCmnS+XfSq5Kbsf6//xw==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.840.0.tgz", + "integrity": "sha512-wbnUiPGLVea6mXbUh04fu+VJmGkQvmToPeTYdHE8eRZq3NRDi3t3WltT+jArLBKD/4NppRpMjf2ju4coMCz91g==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/node-http-handler": "^4.0.6", "@smithy/property-provider": "^4.0.4", @@ -1957,19 +3017,21 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.839.0.tgz", - "integrity": "sha512-GHm0hF4CiDxIDR7TauMaA6iI55uuSqRxMBcqTAHaTPm6+h1A+MS+ysQMxZ+Jvwtoy8WmfTIGrJVxSCw0sK2hvA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.840.0.tgz", + "integrity": "sha512-7F290BsWydShHb+7InXd+IjJc3mlEIm9I0R57F/Pjl1xZB69MdkhVGCnuETWoBt4g53ktJd6NEjzm/iAhFXFmw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/credential-provider-env": "3.839.0", - "@aws-sdk/credential-provider-http": "3.839.0", - "@aws-sdk/credential-provider-process": "3.839.0", - "@aws-sdk/credential-provider-sso": "3.839.0", - "@aws-sdk/credential-provider-web-identity": "3.839.0", - "@aws-sdk/nested-clients": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/credential-provider-env": "3.840.0", + "@aws-sdk/credential-provider-http": "3.840.0", + "@aws-sdk/credential-provider-process": "3.840.0", + "@aws-sdk/credential-provider-sso": "3.840.0", + "@aws-sdk/credential-provider-web-identity": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/credential-provider-imds": "^4.0.6", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", @@ -1980,18 +3042,20 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.839.0.tgz", - "integrity": "sha512-7bR+U2h+ft0V8chyeu9Bh/pvau4ZkQMeRt5f0dAULoepZQ77QQVRP4H04yJPTg9DCtqbVULQ3uf5YOp1/08vQw==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.840.0.tgz", + "integrity": "sha512-KufP8JnxA31wxklLm63evUPSFApGcH8X86z3mv9SRbpCm5ycgWIGVCTXpTOdgq6rPZrwT9pftzv2/b4mV/9clg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.839.0", - "@aws-sdk/credential-provider-http": "3.839.0", - "@aws-sdk/credential-provider-ini": "3.839.0", - "@aws-sdk/credential-provider-process": "3.839.0", - "@aws-sdk/credential-provider-sso": "3.839.0", - "@aws-sdk/credential-provider-web-identity": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/credential-provider-env": "3.840.0", + "@aws-sdk/credential-provider-http": "3.840.0", + "@aws-sdk/credential-provider-ini": "3.840.0", + "@aws-sdk/credential-provider-process": "3.840.0", + "@aws-sdk/credential-provider-sso": "3.840.0", + "@aws-sdk/credential-provider-web-identity": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/credential-provider-imds": "^4.0.6", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", @@ -2002,13 +3066,15 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.839.0.tgz", - "integrity": "sha512-qShpekjociUZ+isyQNa0P7jo+0q3N2+0eJDg8SGyP6K6hHTcGfiqxTDps+IKl6NreCPhZCBzyI9mWkP0xSDR6g==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.840.0.tgz", + "integrity": "sha512-HkDQWHy8tCI4A0Ps2NVtuVYMv9cB4y/IuD/TdOsqeRIAT12h8jDb98BwQPNLAImAOwOWzZJ8Cu0xtSpX7CQhMw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", @@ -2018,15 +3084,17 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.839.0.tgz", - "integrity": "sha512-w10zBLHhU8SBQcdrSPMI02haLoRGZg+gP7mH/Er8VhIXfHefbr7o4NirmB0hwdw/YAH8MLlC9jj7c2SJlsNhYA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.840.0.tgz", + "integrity": "sha512-2qgdtdd6R0Z1y0KL8gzzwFUGmhBHSUx4zy85L2XV1CXhpRNwV71SVWJqLDVV5RVWVf9mg50Pm3AWrUC0xb0pcA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.839.0", - "@aws-sdk/core": "3.839.0", - "@aws-sdk/token-providers": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/client-sso": "3.840.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/token-providers": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", @@ -2036,14 +3104,16 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.839.0.tgz", - "integrity": "sha512-EvqTc7J1kgmiuxknpCp1S60hyMQvmKxsI5uXzQtcogl/N55rxiXEqnCLI5q6p33q91PJegrcMCM5Q17Afhm5qA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.840.0.tgz", + "integrity": "sha512-dpEeVXG8uNZSmVXReE4WP0lwoioX2gstk4RnUgrdUE3YaPq8A+hJiVAyc3h+cjDeIqfbsQbZm9qFetKC2LF9dQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/nested-clients": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -2052,12 +3122,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", - "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.840.0.tgz", + "integrity": "sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -2066,12 +3138,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-logger": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", - "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-logger": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.840.0.tgz", + "integrity": "sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, @@ -2079,12 +3153,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", - "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.840.0.tgz", + "integrity": "sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -2093,14 +3169,16 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.839.0.tgz", - "integrity": "sha512-2u74uRM1JWq6Sf7+3YpjejPM9YkomGt4kWhrmooIBEq1k5r2GTbkH7pNCxBQwBueXM21jAGVDxxeClpTx+5hig==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.840.0.tgz", + "integrity": "sha512-hiiMf7BP5ZkAFAvWRcK67Mw/g55ar7OCrvrynC92hunx/xhMkrgSLM0EXIZ1oTn3uql9kH/qqGF0nqsK6K555A==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", "@smithy/core": "^3.6.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", @@ -2110,23 +3188,25 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/nested-clients": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.839.0.tgz", - "integrity": "sha512-Glic0pg2THYP3aRhJORwJJBe1JLtJoEdWV/MFZNyzCklfMwEzpWtZAyxy+tQyFmMeW50uBAnh2R0jhMMcf257w==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/nested-clients": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.840.0.tgz", + "integrity": "sha512-LXYYo9+n4hRqnRSIMXLBb+BLz+cEmjMtTudwK1BF6Bn2RfdDv29KuyeDRrPCS3TwKl7ZKmXUmE9n5UuHAPfBpA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.839.0", - "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-logger": "3.821.0", - "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-user-agent": "3.839.0", - "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@aws-sdk/util-user-agent-browser": "3.821.0", - "@aws-sdk/util-user-agent-node": "3.839.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.6.0", "@smithy/fetch-http-handler": "^5.0.4", @@ -2158,12 +3238,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", - "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.840.0.tgz", + "integrity": "sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/node-config-provider": "^4.1.3", "@smithy/types": "^4.3.1", "@smithy/util-config-provider": "^4.0.0", @@ -2174,44 +3256,31 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", - "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", - "dependencies": { - "@aws-sdk/types": "3.821.0", - "@smithy/types": "^4.3.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.839.0.tgz", - "integrity": "sha512-MuunkIG1bJVMtTH7MbjXOrhHleU5wjHz5eCAUc6vj7M9rwol71nqjj9b8RLnkO5gsJcKc29Qk8iV6xQuzKWNMw==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/token-providers": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.840.0.tgz", + "integrity": "sha512-6BuTOLTXvmgwjK7ve7aTg9JaWFdM5UoMolLVPMyh3wTv9Ufalh8oklxYHUBIxsKkBGO2WiHXytveuxH6tAgTYg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.839.0", - "@aws-sdk/types": "3.821.0", - "@smithy/node-config-provider": "^4.1.3", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/abort-controller": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", - "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/types": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.840.0.tgz", + "integrity": "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -2220,62 +3289,66 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/node-http-handler": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", - "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-endpoints": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.840.0.tgz", + "integrity": "sha512-eqE9ROdg/Kk0rj3poutyRCFauPDXIf/WSvCqFiRDDVi6QOnCv/M0g2XW8/jSvkJlOyaXkNCptapIp6BeeFFGYw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/querystring-builder": "^4.0.4", + "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", + "@smithy/util-endpoints": "^3.0.6", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/types": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", - "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.840.0.tgz", + "integrity": "sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { - "version": "3.731.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.731.0.tgz", - "integrity": "sha512-NrdkJg6oOUbXR2r9WvHP408CLyvST8cJfp1/jP9pemtjvjPoh6NukbCtiSFdOOb1eryP02CnqQWItfJC1p2Y/Q==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.840.0.tgz", + "integrity": "sha512-Fy5JUEDQU1tPm2Yw/YqRYYc27W5+QD/J4mYvQvdWjUGZLB5q3eLFMGD35Uc28ZFoGMufPr4OCxK/bRfWROBRHQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.0.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { - "version": "3.731.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.731.0.tgz", - "integrity": "sha512-riztxTAfncFS9yQWcBJffGgOgLoKSa63ph+rxWJxKl6BHAmWEvHICj1qDcVmnWfIcvJ5cClclY75l9qKaUH7rQ==", - "dependencies": { - "@aws-sdk/types": "3.731.0", - "@smithy/types": "^4.0.0", - "@smithy/util-endpoints": "^3.0.0", - "tslib": "^2.6.2" }, - "engines": { - "node": ">=18.0.0" + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/abort-controller": { + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/abort-controller": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -2284,10 +3357,12 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/node-http-handler": { + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/node-http-handler": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.0.4", "@smithy/protocol-http": "^5.1.2", @@ -2299,10 +3374,12 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/types": { + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/types": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -4035,9 +5112,9 @@ "link": true }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.116", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.116.tgz", - "integrity": "sha512-wJoNfbDt/OBEuaseXpeMJTYYndpuoAdPNQkVJdRYAgajzCvWZp/yOdgHu4JNoRo949rLYVRidLTxJo7YVc/LQA==", + "version": "0.2.118", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.118.tgz", + "integrity": "sha512-Ee1wQxZelTk+jg89qdiVLH1Z4pPWdP/UyOmWmVac2teT/qrlZdAz2fmw9ub+Cr9ScKfM1d6IFqRuWnXzzS1uuw==", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes-types": "^0.1.50", @@ -28707,7 +29784,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.116", + "@aws/language-server-runtimes": "^0.2.118", "@aws/lsp-core": "^0.0.12", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", diff --git a/package.json b/package.json index 82468102ab..5dacc08bd1 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,14 @@ "ci:generate:agentic:attribution": "ts-node ./script/prepare-agentic-attribution-dependencies.ts && ./script/generate-agentic-attribution.sh && git restore package.json" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.116", + "@aws/language-server-runtimes": "^0.2.118", "@smithy/types": "4.2.0", "clean": "^4.0.2", "typescript": "^5.8.2" }, "devDependencies": { + "@aws-sdk/client-iam": "^3.840.0", + "@aws-sdk/client-sts": "^3.840.0", "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", "@types/ignore-walk": "^4.0.3", diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 2616a67b26..aaf0294add 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -36,7 +36,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.116", + "@aws/language-server-runtimes": "^0.2.118", "@aws/lsp-core": "^0.0.12", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts index 00ac935f7c..535d17ce56 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -223,7 +223,7 @@ import { sanitize } from '@aws/lsp-core/out/util/path' import { getLatestAvailableModel } from './utils/agenticChatControllerHelper' import { ActiveUserTracker } from '../../shared/activeUserTracker' import { UserContext } from '../../client/token/codewhispererbearertokenclient' -import { CodeWhispererServiceToken } from '../../shared/codeWhispererService' +import { CodeWhispererServiceToken } from '../../shared/codeWhispererService/codeWhispererServiceToken' type ChatHandlers = Omit< LspHandlers, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts index 3a6a8eee7a..6741db106e 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReview.ts @@ -1,6 +1,6 @@ /* eslint-disable import/no-nodejs-modules */ -import { CodeWhispererServiceToken } from '../../../../shared/codeWhispererService' +import { CodeWhispererServiceToken } from '../../../../shared/codeWhispererService/codeWhispererServiceToken' import { Features } from '@aws/language-server-runtimes/server-interface/server' import { CODE_REVIEW_TOOL_NAME, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts index c49927eefd..ff9c78987f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts @@ -11,7 +11,7 @@ import { McpTool } from './mcp/mcpTool' import { FileSearch, FileSearchParams } from './fileSearch' import { GrepSearch } from './grepSearch' import { CodeReview } from './qCodeAnalysis/codeReview' -import { CodeWhispererServiceToken } from '../../../shared/codeWhispererService' +import { CodeWhispererServiceToken } from '../../../shared/codeWhispererService/codeWhispererServiceToken' import { McpToolDefinition } from './mcp/mcpTypes' import { getGlobalAgentConfigPath, diff --git a/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.test.ts b/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.test.ts index 97d3a084d6..72decf69c1 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.test.ts @@ -7,7 +7,7 @@ import { ServerConfigurationProvider, } from './qConfigurationServer' import { TestFeatures } from '@aws/language-server-runtimes/testing' -import { CodeWhispererServiceToken } from '../../shared/codeWhispererService' +import { CodeWhispererServiceToken } from '../../shared/codeWhispererService/codeWhispererServiceToken' import { CancellationToken, CancellationTokenSource, diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts index 31bc0a224a..5874065963 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts @@ -1,5 +1,5 @@ import assert = require('assert') -import { FileContext } from '../../../shared/codeWhispererService' +import { FileContext } from '../../../shared/codeWhispererService/codeWhispererServiceBase' import { autoTrigger, getAutoTriggerType, triggerType } from './autoTrigger' describe('Auto Trigger', async () => { diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts index d9dd7f16d5..2e1492cb4a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts @@ -1,6 +1,6 @@ import * as os from 'os' import { Logging } from '@aws/language-server-runtimes/server-interface' -import { FileContext } from '../../../shared/codeWhispererService' +import { FileContext } from '../../../shared/codeWhispererService/codeWhispererServiceBase' import typedCoefficients = require('./coefficients.json') import { TextDocumentContentChangeEvent } from 'vscode-languageserver-textdocument' diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.test.ts index 261748468f..d3b471e3ff 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert' import * as sinon from 'sinon' import { editPredictionAutoTrigger } from './editPredictionAutoTrigger' import { EditPredictionConfigManager } from './editPredictionConfig' -import { FileContext } from '../../../shared/codeWhispererService' +import { FileContext } from '../../../shared/codeWhispererService/codeWhispererServiceBase' import { Position } from '@aws/language-server-runtimes/server-interface' import { CursorTracker } from '../tracker/cursorTracker' import { RecentEditTracker } from '../tracker/codeEditTracker' diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.ts index eee9edbdcb..8a64697d27 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { FileContext } from '../../../shared/codeWhispererService' +import { FileContext } from '../../../shared/codeWhispererService/codeWhispererServiceBase' import { Position } from '@aws/language-server-runtimes/server-interface' import { CursorTracker } from '../tracker/cursorTracker' import { RecentEditTracker } from '../tracker/codeEditTracker' diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts index 2db0cfaba9..3b74cc9b91 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts @@ -15,11 +15,11 @@ import sinon, { StubbedInstance } from 'ts-sinon' import { CONTEXT_CHARACTERS_LIMIT, CodewhispererServerFactory } from './codeWhispererServer' import { CodeWhispererServiceBase, - CodeWhispererServiceToken, ResponseContext, Suggestion, SuggestionType, -} from '../../shared/codeWhispererService' +} from '../../shared/codeWhispererService/codeWhispererServiceBase' +import { CodeWhispererServiceToken } from '../../shared/codeWhispererService/codeWhispererServiceToken' import { CodeWhispererSession, SessionData, SessionManager } from './session/sessionManager' import { EMPTY_RESULT, diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index 7259aa6a8c..5a95a6d424 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -16,12 +16,12 @@ import { } from '@aws/language-server-runtimes/server-interface' import { autoTrigger, getAutoTriggerType, getNormalizeOsName, triggerType } from './auto-trigger/autoTrigger' import { - CodeWhispererServiceToken, GenerateSuggestionsRequest, GenerateSuggestionsResponse, Suggestion, SuggestionType, -} from '../../shared/codeWhispererService' +} from '../../shared/codeWhispererService/codeWhispererServiceBase' +import { CodeWhispererServiceToken } from '../../shared/codeWhispererService/codeWhispererServiceToken' import { CodewhispererLanguage, getRuntimeLanguage, getSupportedLanguageId } from '../../shared/languageDetection' import { mergeEditSuggestionsWithFileContext, truncateOverlapWithRightContext } from './mergeRightUtils' import { CodeWhispererSession, SessionManager } from './session/sessionManager' @@ -895,9 +895,9 @@ export const CodewhispererServerFactory = } logging.debug(`CodePercentageTracker customizationArn updated to ${customizationArn}`) /* - The flag enableTelemetryEventsToDestination is set to true temporarily. It's value will be determined through destination - configuration post all events migration to STE. It'll be replaced by qConfig['enableTelemetryEventsToDestination'] === true - */ + The flag enableTelemetryEventsToDestination is set to true temporarily. It's value will be determined through destination + configuration post all events migration to STE. It'll be replaced by qConfig['enableTelemetryEventsToDestination'] === true + */ // const enableTelemetryEventsToDestination = true // telemetryService.updateEnableTelemetryEventsToDestination(enableTelemetryEventsToDestination) telemetryService.updateOptOutPreference(optOutTelemetryPreference) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/mergeRightUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/mergeRightUtils.ts index 9091139e7e..f8cb037786 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/mergeRightUtils.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/mergeRightUtils.ts @@ -1,7 +1,7 @@ import { InlineCompletionItemWithReferences, TextDocument } from '@aws/language-server-runtimes/server-interface' import { CodeWhispererSession } from './session/sessionManager' import { applyUnifiedDiff, generateUnifiedDiffWithTimestamps } from './diffUtils' -import { FileContext } from '../../shared/codeWhispererService' +import { FileContext } from '../../shared/codeWhispererService/codeWhispererServiceBase' /** * Returns the longest overlap between the Suffix of firstString and Prefix of second string diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts index db34f885ff..5b633fbe8f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts @@ -1,6 +1,6 @@ import * as assert from 'assert' import sinon from 'ts-sinon' -import { Suggestion } from '../../../shared/codeWhispererService' +import { Suggestion } from '../../../shared/codeWhispererService/codeWhispererServiceBase' import { CodeWhispererSession, SessionData, SessionManager } from './sessionManager' import { TextDocument } from '@aws/language-server-runtimes/server-interface' import { HELLO_WORLD_IN_CSHARP } from '../../../shared/testUtils' diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts index 35568fc6e2..5dbf0557ad 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts @@ -6,7 +6,11 @@ import { } from '@aws/language-server-runtimes/server-interface' import { v4 as uuidv4 } from 'uuid' import { CodewhispererAutomatedTriggerType, CodewhispererTriggerType } from '../auto-trigger/autoTrigger' -import { GenerateSuggestionsRequest, ResponseContext, Suggestion } from '../../../shared/codeWhispererService' +import { + GenerateSuggestionsRequest, + ResponseContext, + Suggestion, +} from '../../../shared/codeWhispererService/codeWhispererServiceBase' import { CodewhispererLanguage } from '../../../shared/languageDetection' import { CodeWhispererSupplementalContext } from '../../../shared/models/model' import { Logging } from '@aws/language-server-runtimes/server-interface' diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts index ad31fd15af..462e2f918f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/userTriggerDecision.test.ts @@ -15,7 +15,7 @@ import { ResponseContext, Suggestion, SuggestionType, -} from '../../shared/codeWhispererService' +} from '../../shared/codeWhispererService/codeWhispererServiceBase' import { CodeWhispererSession, SessionManager } from './session/sessionManager' import { TelemetryService } from '../../shared/telemetry/telemetryService' import { initBaseTestServiceManager, TestAmazonQServiceManager } from '../../shared/amazonQServiceManager/testUtils' diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/transformHandler.test.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/transformHandler.test.ts index efaa69e3cf..f42f351278 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/transformHandler.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/transformHandler.test.ts @@ -14,7 +14,7 @@ import * as fs from 'fs' import got from 'got' import { StubbedInstance, default as simon, stubInterface } from 'ts-sinon' import { StreamingClient, createStreamingClient } from '../../../client/streamingClient/codewhispererStreamingClient' -import { CodeWhispererServiceToken } from '../../../shared/codeWhispererService' +import { CodeWhispererServiceToken } from '../../../shared/codeWhispererService/codeWhispererServiceToken' import { CancelTransformRequest, CancellationJobStatus, diff --git a/server/aws-lsp-codewhisperer/src/language-server/securityScan/securityScanHandler.test.ts b/server/aws-lsp-codewhisperer/src/language-server/securityScan/securityScanHandler.test.ts index 6b6d045c3d..7715541476 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/securityScan/securityScanHandler.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/securityScan/securityScanHandler.test.ts @@ -5,7 +5,7 @@ import got from 'got' import * as Sinon from 'sinon' import { StubbedInstance, default as simon, stubInterface } from 'ts-sinon' import { StartCodeAnalysisRequest } from '../../client/token/codewhispererbearertokenclient' -import { CodeWhispererServiceToken } from '../../shared/codeWhispererService' +import { CodeWhispererServiceToken } from '../../shared/codeWhispererService/codeWhispererServiceToken' import { SecurityScanHandler } from './securityScanHandler' import { RawCodeScanIssue } from './types' import * as ScanConstants from './constants' diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts index 0b4ec53f25..9764734254 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts @@ -5,7 +5,7 @@ import { CredentialsProvider, Logging } from '@aws/language-server-runtimes/serv import { DependencyDiscoverer } from './dependency/dependencyDiscoverer' import { WorkspaceFolder } from 'vscode-languageserver-protocol' import { ArtifactManager } from './artifactManager' -import { CodeWhispererServiceToken } from '../../shared/codeWhispererService' +import { CodeWhispererServiceToken } from '../../shared/codeWhispererService/codeWhispererServiceToken' import { CreateWorkspaceResponse } from '../../client/token/codewhispererbearertokenclient' import { AWSError } from 'aws-sdk' diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQIAMServiceManager.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQIAMServiceManager.ts index 774102a987..a7298bb1b5 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQIAMServiceManager.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQIAMServiceManager.ts @@ -1,4 +1,4 @@ -import { CodeWhispererServiceIAM } from '../codeWhispererService' +import { CodeWhispererServiceIAM } from '../codeWhispererService/codeWhispererServiceIAM' import { AmazonQBaseServiceManager, BaseAmazonQServiceManager, diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.test.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.test.ts index 73af1c14fc..9cf10fc623 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.test.ts @@ -2,7 +2,8 @@ import * as assert from 'assert' import sinon, { StubbedInstance, stubInterface } from 'ts-sinon' import { AmazonQTokenServiceManager } from './AmazonQTokenServiceManager' import { TestFeatures } from '@aws/language-server-runtimes/testing' -import { CodeWhispererServiceToken, GenerateSuggestionsRequest } from '../codeWhispererService' +import { GenerateSuggestionsRequest } from '../codeWhispererService/codeWhispererServiceBase' +import { CodeWhispererServiceToken } from '../codeWhispererService/codeWhispererServiceToken' import { AmazonQServiceInitializationError, AmazonQServicePendingProfileError, diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts index 8db60a69ab..7b182f85dd 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts @@ -8,7 +8,7 @@ import { InitializeParams, CancellationTokenSource, } from '@aws/language-server-runtimes/server-interface' -import { CodeWhispererServiceToken } from '../codeWhispererService' +import { CodeWhispererServiceToken } from '../codeWhispererService/codeWhispererServiceToken' import { AmazonQError, AmazonQServiceAlreadyInitializedError, diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.test.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.test.ts index acc55d75ed..dda7cbf52c 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.test.ts @@ -1,7 +1,7 @@ import { TestFeatures } from '@aws/language-server-runtimes/testing' import sinon, { StubbedInstance } from 'ts-sinon' import { expect } from 'chai' -import { CodeWhispererServiceBase } from '../codeWhispererService' +import { CodeWhispererServiceBase } from '../codeWhispererService/codeWhispererServiceBase' import { stubCodeWhispererService } from '../testUtils' import { initBaseTestServiceManager, TestAmazonQServiceManager } from './testUtils' import { diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.ts index 9c241809a7..0413e8f9bf 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/BaseAmazonQServiceManager.ts @@ -9,7 +9,7 @@ import { UpdateConfigurationParams, Workspace, } from '@aws/language-server-runtimes/server-interface' -import { CodeWhispererServiceBase } from '../codeWhispererService' +import { CodeWhispererServiceBase } from '../codeWhispererService/codeWhispererServiceBase' import { AmazonQConfigurationCache, AmazonQWorkspaceConfig, diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/qDeveloperProfiles.test.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/qDeveloperProfiles.test.ts index 022b3dedfa..e42bfed80b 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/qDeveloperProfiles.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/qDeveloperProfiles.test.ts @@ -1,6 +1,6 @@ import * as assert from 'assert' import sinon, { StubbedInstance, stubInterface } from 'ts-sinon' -import { CodeWhispererServiceToken } from '../codeWhispererService' +import { CodeWhispererServiceToken } from '../codeWhispererService/codeWhispererServiceToken' import { SsoConnectionType } from '../utils' import { AWSInitializationOptions, diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/qDeveloperProfiles.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/qDeveloperProfiles.ts index 47f5ec7c37..e7048afe52 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/qDeveloperProfiles.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/qDeveloperProfiles.ts @@ -7,7 +7,7 @@ import { } from '@aws/language-server-runtimes/server-interface' import { isBool, isObject, SsoConnectionType } from '../utils' import { AWS_Q_ENDPOINTS } from '../../shared/constants' -import { CodeWhispererServiceToken } from '../codeWhispererService' +import { CodeWhispererServiceToken } from '../codeWhispererService/codeWhispererServiceToken' import { AmazonQServiceProfileThrottlingError } from './errors' export interface AmazonQDeveloperProfile { diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/testUtils.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/testUtils.ts index 78870b62e8..622e325780 100644 --- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/testUtils.ts +++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/testUtils.ts @@ -1,5 +1,5 @@ import { TestFeatures } from '@aws/language-server-runtimes/testing' -import { CodeWhispererServiceBase } from '../codeWhispererService' +import { CodeWhispererServiceBase } from '../codeWhispererService/codeWhispererServiceBase' import { BaseAmazonQServiceManager, QServiceManagerFeatures } from './BaseAmazonQServiceManager' import { StreamingClientServiceBase } from '../streamingClientService' import { diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.test.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.test.ts deleted file mode 100644 index 63ad6556e7..0000000000 --- a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.test.ts +++ /dev/null @@ -1,398 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - CredentialsProvider, - CredentialsType, - Workspace, - Logging, - SDKInitializator, -} from '@aws/language-server-runtimes/server-interface' -import { ConfigurationOptions } from 'aws-sdk' -import * as sinon from 'sinon' -import * as assert from 'assert' -import { - CodeWhispererServiceBase, - CodeWhispererServiceToken, - CodeWhispererServiceIAM, - GenerateSuggestionsRequest, - GenerateSuggestionsResponse, -} from './codeWhispererService' - -describe('CodeWhispererService', function () { - let sandbox: sinon.SinonSandbox - let mockCredentialsProvider: sinon.SinonStubbedInstance - let mockWorkspace: sinon.SinonStubbedInstance - let mockLogging: sinon.SinonStubbedInstance - let mockSDKInitializator: sinon.SinonStubbedInstance - - beforeEach(function () { - sandbox = sinon.createSandbox() - - mockCredentialsProvider = { - getCredentials: sandbox.stub(), - hasCredentials: sandbox.stub(), - refresh: sandbox.stub(), - } as any - - mockWorkspace = { - getWorkspaceFolder: sandbox.stub(), - getWorkspaceFolders: sandbox.stub(), - } as any - - mockLogging = { - debug: sandbox.stub(), - error: sandbox.stub(), - info: sandbox.stub(), - warn: sandbox.stub(), - log: sandbox.stub(), - } - - mockSDKInitializator = { - initialize: sandbox.stub(), - } as any - }) - - afterEach(function () { - sandbox.restore() - }) - - describe('CodeWhispererServiceBase', function () { - let service: CodeWhispererServiceBase - - beforeEach(function () { - // Create a concrete implementation for testing abstract class - class TestCodeWhispererService extends CodeWhispererServiceBase { - client: any = {} - - getCredentialsType(): CredentialsType { - return 'iam' - } - - // Add public getters for protected properties - get testCodeWhispererRegion() { - return this.codeWhispererRegion - } - - get testCodeWhispererEndpoint() { - return this.codeWhispererEndpoint - } - - async generateCompletionsAndEdits(): Promise { - return { - suggestions: [], - responseContext: { requestId: 'test', codewhispererSessionId: 'test' }, - } - } - - async generateSuggestions(): Promise { - return { - suggestions: [], - responseContext: { requestId: 'test', codewhispererSessionId: 'test' }, - } - } - - clearCachedSuggestions(): void {} - } - - service = new TestCodeWhispererService('us-east-1', 'https://codewhisperer.us-east-1.amazonaws.com') - }) - - describe('constructor', function () { - it('should initialize with region and endpoint', function () { - assert.strictEqual((service as any).testCodeWhispererRegion, 'us-east-1') - assert.strictEqual( - (service as any).testCodeWhispererEndpoint, - 'https://codewhisperer.us-east-1.amazonaws.com' - ) - }) - }) - - describe('request tracking', function () { - it('should track inflight requests', function () { - const mockRequest = { - abort: sandbox.stub(), - } as any - - service.trackRequest(mockRequest) - assert.strictEqual(service.inflightRequests.size, 1) - assert.strictEqual(service.inflightRequests.has(mockRequest), true) - }) - - it('should complete and remove tracked requests', function () { - const mockRequest = { - abort: sandbox.stub(), - } as any - - service.trackRequest(mockRequest) - service.completeRequest(mockRequest) - - assert.strictEqual(service.inflightRequests.size, 0) - assert.strictEqual(service.inflightRequests.has(mockRequest), false) - }) - - it('should abort all inflight requests', function () { - const mockRequest1 = { abort: sandbox.stub() } as any - const mockRequest2 = { abort: sandbox.stub() } as any - - service.trackRequest(mockRequest1) - service.trackRequest(mockRequest2) - - service.abortInflightRequests() - - assert.strictEqual(mockRequest1.abort.calledOnce, true) - assert.strictEqual(mockRequest2.abort.calledOnce, true) - assert.strictEqual(service.inflightRequests.size, 0) - }) - }) - - describe('updateClientConfig', function () { - it('should update client configuration', function () { - const mockClient = { - config: { - update: sandbox.stub(), - }, - // Add minimal required properties to satisfy the interface - createCodeScan: sandbox.stub(), - createCodeScanUploadUrl: sandbox.stub(), - createProfile: sandbox.stub(), - deleteProfile: sandbox.stub(), - generateCompletions: sandbox.stub(), - generateSuggestions: sandbox.stub(), - getCodeAnalysis: sandbox.stub(), - getCodeScan: sandbox.stub(), - listCodeAnalysisFindings: sandbox.stub(), - listCodeScans: sandbox.stub(), - listFeatureEvaluations: sandbox.stub(), - listProfiles: sandbox.stub(), - sendTelemetryEvent: sandbox.stub(), - startCodeAnalysis: sandbox.stub(), - stopCodeAnalysis: sandbox.stub(), - updateProfile: sandbox.stub(), - } as any - service.client = mockClient - - const options: ConfigurationOptions = { region: 'us-west-2' } - service.updateClientConfig(options) - - assert.strictEqual(mockClient.config.update.calledOnceWith(options), true) - }) - }) - - describe('generateItemId', function () { - it('should generate unique item IDs', function () { - const id1 = service.generateItemId() - const id2 = service.generateItemId() - - assert.strictEqual(typeof id1, 'string') - assert.strictEqual(typeof id2, 'string') - assert.notStrictEqual(id1, id2) - }) - }) - }) - - describe('CodeWhispererServiceIAM', function () { - let service: CodeWhispererServiceIAM - - beforeEach(function () { - // Mock the createCodeWhispererSigv4Client function to avoid real client creation - const mockClient = { - generateRecommendations: sandbox.stub().returns({ - promise: sandbox.stub().resolves({ - recommendations: [], - $response: { - requestId: 'test-request-id', - httpResponse: { - headers: { 'x-amzn-sessionid': 'test-session-id' }, - }, - }, - }), - }), - setupRequestListeners: sandbox.stub(), - config: { - update: sandbox.stub(), - }, - } - - // Mock the client creation - const createClientStub = sandbox.stub( - require('../client/sigv4/codewhisperer'), - 'createCodeWhispererSigv4Client' - ) - createClientStub.returns(mockClient) - - service = new CodeWhispererServiceIAM( - mockCredentialsProvider as any, - {} as any, // workspace parameter - mockLogging as any, - 'us-east-1', - 'https://codewhisperer.us-east-1.amazonaws.com', - mockSDKInitializator as any - ) - }) - - describe('getCredentialsType', function () { - it('should return iam credentials type', function () { - assert.strictEqual(service.getCredentialsType(), 'iam') - }) - }) - - describe('generateSuggestions', function () { - it('should call client.generateRecommendations and process response', async function () { - const mockRequest: GenerateSuggestionsRequest = { - fileContext: { - filename: 'test.js', - programmingLanguage: { languageName: 'javascript' }, - leftFileContent: 'const x = ', - rightFileContent: '', - }, - maxResults: 5, - } - - const result = await service.generateSuggestions(mockRequest) - - assert.strictEqual(Array.isArray(result.suggestions), true) - assert.strictEqual(typeof result.responseContext.requestId, 'string') - assert.strictEqual(typeof result.responseContext.codewhispererSessionId, 'string') - }) - - it('should add customizationArn to request if set', async function () { - service.customizationArn = 'test-arn' - - const mockRequest: GenerateSuggestionsRequest = { - fileContext: { - filename: 'test.js', - programmingLanguage: { languageName: 'javascript' }, - leftFileContent: 'const x = ', - rightFileContent: '', - }, - maxResults: 5, - } - - await service.generateSuggestions(mockRequest) - - // Verify that the client was called with the customizationArn - const clientCall = (service.client.generateRecommendations as sinon.SinonStub).getCall(0) - assert.strictEqual(clientCall.args[0].customizationArn, 'test-arn') - }) - }) - }) - - describe('CodeWhispererServiceToken', function () { - let service: CodeWhispererServiceToken - let mockClient: any - - beforeEach(function () { - // Mock the token client - mockClient = { - generateCompletions: sandbox.stub().returns({ - promise: sandbox.stub().resolves({ - completions: [ - { - content: 'console.log("hello");', - references: [], - }, - ], - $response: { - requestId: 'test-request-id', - httpResponse: { - headers: { 'x-amzn-sessionid': 'test-session-id' }, - }, - }, - }), - }), - config: { - update: sandbox.stub(), - }, - } - - // Mock the client creation - const createTokenClientStub = sandbox.stub( - require('../client/token/codewhisperer'), - 'createCodeWhispererTokenClient' - ) - createTokenClientStub.returns(mockClient) - - // Mock bearer credentials - mockCredentialsProvider.getCredentials.returns({ - token: 'mock-bearer-token', - }) - - service = new CodeWhispererServiceToken( - mockCredentialsProvider as any, - mockWorkspace as any, - mockLogging as any, - 'us-east-1', - 'https://codewhisperer.us-east-1.amazonaws.com', - mockSDKInitializator as any - ) - }) - - describe('getCredentialsType', function () { - it('should return bearer credentials type', function () { - assert.strictEqual(service.getCredentialsType(), 'bearer') - }) - }) - - describe('generateSuggestions', function () { - it('should call client.generateCompletions and process response', async function () { - const mockRequest: GenerateSuggestionsRequest = { - fileContext: { - filename: 'test.js', - programmingLanguage: { languageName: 'javascript' }, - leftFileContent: 'const x = ', - rightFileContent: '', - }, - maxResults: 5, - } - - const result = await service.generateSuggestions(mockRequest) - - assert.strictEqual(mockClient.generateCompletions.calledOnce, true) - assert.strictEqual(Array.isArray(result.suggestions), true) - assert.strictEqual(typeof result.responseContext.requestId, 'string') - assert.strictEqual(typeof result.responseContext.codewhispererSessionId, 'string') - }) - - it('should add customizationArn to request if set', async function () { - service.customizationArn = 'test-arn' - - const mockRequest: GenerateSuggestionsRequest = { - fileContext: { - filename: 'test.js', - programmingLanguage: { languageName: 'javascript' }, - leftFileContent: 'const x = ', - rightFileContent: '', - }, - maxResults: 5, - } - - await service.generateSuggestions(mockRequest) - - const clientCall = mockClient.generateCompletions.getCall(0) - assert.strictEqual(clientCall.args[0].customizationArn, 'test-arn') - }) - - it('should process profile ARN with withProfileArn method', async function () { - const mockRequest: GenerateSuggestionsRequest = { - fileContext: { - filename: 'test.js', - programmingLanguage: { languageName: 'javascript' }, - leftFileContent: 'const x = ', - rightFileContent: '', - }, - maxResults: 5, - } - - const withProfileArnStub = sandbox.stub(service, 'withProfileArn' as any) - withProfileArnStub.returns(mockRequest) - - await service.generateSuggestions(mockRequest) - - assert.strictEqual(withProfileArnStub.calledOnceWith(mockRequest), true) - }) - }) - }) -}) diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceBase.test.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceBase.test.ts new file mode 100644 index 0000000000..a90f0e7112 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceBase.test.ts @@ -0,0 +1,272 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CredentialsType } from '@aws/language-server-runtimes/server-interface' +import { AWSError, ConfigurationOptions } from 'aws-sdk' +import * as sinon from 'sinon' +import * as assert from 'assert' +import { PromiseResult } from 'aws-sdk/lib/request' +import { + CodeWhispererServiceBase, + GenerateSuggestionsResponse, + CreateUploadUrlRequest, + CreateUploadUrlResponse, + StartTransformationRequest, + StartTransformationResponse, + StopTransformationRequest, + StopTransformationResponse, + GetTransformationRequest, + GetTransformationResponse, + GetTransformationPlanRequest, + GetTransformationPlanResponse, + StartCodeAnalysisRequest, + StartCodeAnalysisResponse, + GetCodeAnalysisRequest, + GetCodeAnalysisResponse, + ListCodeAnalysisFindingsRequest, + ListCodeAnalysisFindingsResponse, + ListAvailableCustomizationsRequest, + ListAvailableCustomizationsResponse, + ListAvailableProfilesRequest, + SendTelemetryEventRequest, + SendTelemetryEventResponse, + CreateWorkspaceRequest, + CreateWorkspaceResponse, + ListWorkspaceMetadataRequest, + ListWorkspaceMetadataResponse, + DeleteWorkspaceRequest, + DeleteWorkspaceResponse, + ListFeatureEvaluationsRequest, + ListFeatureEvaluationsResponse, + CreateSubscriptionTokenRequest, + CreateSubscriptionTokenResponse, + ListAvailableProfilesResponse, +} from './codeWhispererServiceBase' + +describe('CodeWhispererServiceBase', function () { + // Create the environment + let sandbox: sinon.SinonSandbox + let service: CodeWhispererServiceBase + + beforeEach(function () { + sandbox = sinon.createSandbox() + + // Create a concrete implementation for testing abstract class + class TestCodeWhispererService extends CodeWhispererServiceBase { + client: any = {} + + getCredentialsType(): CredentialsType { + return 'iam' + } + + // Add public getters for protected properties + get testCodeWhispererRegion() { + return this.codeWhispererRegion + } + + get testCodeWhispererEndpoint() { + return this.codeWhispererEndpoint + } + + async generateCompletionsAndEdits(): Promise { + return { + suggestions: [], + responseContext: { requestId: 'test', codewhispererSessionId: 'test' }, + } + } + + async generateSuggestions(): Promise { + return { + suggestions: [], + responseContext: { requestId: 'test', codewhispererSessionId: 'test' }, + } + } + + clearCachedSuggestions(): void {} + + override codeModernizerCreateUploadUrl(request: CreateUploadUrlRequest): Promise { + throw new Error('Method not implemented.') + } + override codeModernizerStartCodeTransformation( + request: StartTransformationRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override codeModernizerStopCodeTransformation( + request: StopTransformationRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override codeModernizerGetCodeTransformation( + request: GetTransformationRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override codeModernizerGetCodeTransformationPlan( + request: GetTransformationPlanRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override createUploadUrl( + request: CreateUploadUrlRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override startCodeAnalysis( + request: StartCodeAnalysisRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override getCodeAnalysis( + request: GetCodeAnalysisRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override listCodeAnalysisFindings( + request: ListCodeAnalysisFindingsRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override listAvailableCustomizations( + request: ListAvailableCustomizationsRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override listAvailableProfiles( + request: ListAvailableProfilesRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override sendTelemetryEvent( + request: SendTelemetryEventRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override createWorkspace( + request: CreateWorkspaceRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override listWorkspaceMetadata( + request: ListWorkspaceMetadataRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override deleteWorkspace( + request: DeleteWorkspaceRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override listFeatureEvaluations( + request: ListFeatureEvaluationsRequest + ): Promise> { + throw new Error('Method not implemented.') + } + override createSubscriptionToken( + request: CreateSubscriptionTokenRequest + ): Promise> { + throw new Error('Method not implemented.') + } + } + + service = new TestCodeWhispererService('us-east-1', 'https://codewhisperer.us-east-1.amazonaws.com') + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('constructor', function () { + it('should initialize with region and endpoint', function () { + assert.strictEqual((service as any).testCodeWhispererRegion, 'us-east-1') + assert.strictEqual( + (service as any).testCodeWhispererEndpoint, + 'https://codewhisperer.us-east-1.amazonaws.com' + ) + }) + }) + + describe('request tracking', function () { + it('should track inflight requests', function () { + const mockRequest = { + abort: sandbox.stub(), + } as any + + service.trackRequest(mockRequest) + assert.strictEqual(service.inflightRequests.size, 1) + assert.strictEqual(service.inflightRequests.has(mockRequest), true) + }) + + it('should complete and remove tracked requests', function () { + const mockRequest = { + abort: sandbox.stub(), + } as any + + service.trackRequest(mockRequest) + service.completeRequest(mockRequest) + + assert.strictEqual(service.inflightRequests.size, 0) + assert.strictEqual(service.inflightRequests.has(mockRequest), false) + }) + + it('should abort all inflight requests', function () { + const mockRequest1 = { abort: sandbox.stub() } as any + const mockRequest2 = { abort: sandbox.stub() } as any + + service.trackRequest(mockRequest1) + service.trackRequest(mockRequest2) + + service.abortInflightRequests() + + assert.strictEqual(mockRequest1.abort.calledOnce, true) + assert.strictEqual(mockRequest2.abort.calledOnce, true) + assert.strictEqual(service.inflightRequests.size, 0) + }) + }) + + describe('updateClientConfig', function () { + it('should update client configuration', function () { + const mockClient = { + config: { + update: sandbox.stub(), + }, + // Add minimal required properties to satisfy the interface + createCodeScan: sandbox.stub(), + createCodeScanUploadUrl: sandbox.stub(), + createProfile: sandbox.stub(), + deleteProfile: sandbox.stub(), + generateCompletions: sandbox.stub(), + generateSuggestions: sandbox.stub(), + getCodeAnalysis: sandbox.stub(), + getCodeScan: sandbox.stub(), + listCodeAnalysisFindings: sandbox.stub(), + listCodeScans: sandbox.stub(), + listFeatureEvaluations: sandbox.stub(), + listProfiles: sandbox.stub(), + sendTelemetryEvent: sandbox.stub(), + startCodeAnalysis: sandbox.stub(), + stopCodeAnalysis: sandbox.stub(), + updateProfile: sandbox.stub(), + } as any + service.client = mockClient + + const options: ConfigurationOptions = { region: 'us-west-2' } + service.updateClientConfig(options) + + assert.strictEqual(mockClient.config.update.calledOnceWith(options), true) + }) + }) + + describe('generateItemId', function () { + it('should generate unique item IDs', function () { + const id1 = service.generateItemId() + const id2 = service.generateItemId() + + assert.strictEqual(typeof id1, 'string') + assert.strictEqual(typeof id2, 'string') + assert.notStrictEqual(id1, id2) + }) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceBase.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceBase.ts new file mode 100644 index 0000000000..63803cd825 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceBase.ts @@ -0,0 +1,205 @@ +import { CredentialsType, CancellationToken } from '@aws/language-server-runtimes/server-interface' +import { AWSError, ConfigurationOptions } from 'aws-sdk' +import { PromiseResult } from 'aws-sdk/lib/request' +import { v4 as uuidv4 } from 'uuid' +import { RequestExtras } from '../../client/token/codewhisperer' +import CodeWhispererSigv4Client = require('../../client/sigv4/codewhisperersigv4client') +import CodeWhispererTokenClient = require('../../client/token/codewhispererbearertokenclient') + +export interface Suggestion extends CodeWhispererTokenClient.Completion, CodeWhispererSigv4Client.Recommendation { + itemId: string +} + +export interface GenerateSuggestionsRequest extends CodeWhispererTokenClient.GenerateCompletionsRequest { + // TODO : This is broken due to Interface 'GenerateSuggestionsRequest' cannot simultaneously extend types 'GenerateCompletionsRequest' and 'GenerateRecommendationsRequest'. + //CodeWhispererSigv4Client.GenerateRecommendationsRequest { + maxResults: number +} + +export type FileContext = GenerateSuggestionsRequest['fileContext'] + +export interface ResponseContext { + requestId: string + codewhispererSessionId: string + nextToken?: string +} + +export enum SuggestionType { + EDIT = 'EDIT', + COMPLETION = 'COMPLETION', +} + +export interface GenerateSuggestionsResponse { + suggestions: Suggestion[] + suggestionType?: SuggestionType + responseContext: ResponseContext +} + +// TODO: CodeWhispererSigv4Client requests and responses do not exist yet and should be added in the future +export interface CreateUploadUrlRequest extends CodeWhispererTokenClient.CreateUploadUrlRequest {} +export interface CreateUploadUrlResponse extends CodeWhispererTokenClient.CreateUploadUrlResponse {} + +export interface StartTransformationRequest extends CodeWhispererTokenClient.StartTransformationRequest {} +export interface StartTransformationResponse extends CodeWhispererTokenClient.StartTransformationResponse {} + +export interface StopTransformationRequest extends CodeWhispererTokenClient.StopTransformationRequest {} +export interface StopTransformationResponse extends CodeWhispererTokenClient.StopTransformationResponse {} + +export interface GetTransformationRequest extends CodeWhispererTokenClient.GetTransformationRequest {} +export interface GetTransformationResponse extends CodeWhispererTokenClient.GetTransformationResponse {} + +export interface GetTransformationPlanRequest extends CodeWhispererTokenClient.GetTransformationPlanRequest {} +export interface GetTransformationPlanResponse extends CodeWhispererTokenClient.GetTransformationPlanResponse {} + +export interface StartCodeAnalysisRequest extends CodeWhispererTokenClient.StartCodeAnalysisRequest {} +export interface StartCodeAnalysisResponse extends CodeWhispererTokenClient.StartCodeAnalysisResponse {} + +export interface GetCodeAnalysisRequest extends CodeWhispererTokenClient.GetCodeAnalysisRequest {} +export interface GetCodeAnalysisResponse extends CodeWhispererTokenClient.GetCodeAnalysisResponse {} + +export interface ListCodeAnalysisFindingsRequest extends CodeWhispererTokenClient.ListCodeAnalysisFindingsRequest {} +export interface ListCodeAnalysisFindingsResponse extends CodeWhispererTokenClient.ListCodeAnalysisFindingsResponse {} + +export interface ListAvailableCustomizationsRequest + extends CodeWhispererTokenClient.ListAvailableCustomizationsRequest {} +export interface ListAvailableCustomizationsResponse + extends CodeWhispererTokenClient.ListAvailableCustomizationsResponse {} + +export interface ListAvailableProfilesRequest extends CodeWhispererTokenClient.ListAvailableProfilesRequest {} +export interface ListAvailableProfilesResponse extends CodeWhispererTokenClient.ListAvailableProfilesResponse {} + +export interface SendTelemetryEventRequest extends CodeWhispererTokenClient.SendTelemetryEventRequest {} +export interface SendTelemetryEventResponse extends CodeWhispererTokenClient.SendTelemetryEventResponse {} + +export interface CreateWorkspaceRequest extends CodeWhispererTokenClient.CreateWorkspaceRequest {} +export interface CreateWorkspaceResponse extends CodeWhispererTokenClient.CreateWorkspaceResponse {} + +export interface ListWorkspaceMetadataRequest extends CodeWhispererTokenClient.ListWorkspaceMetadataRequest {} +export interface ListWorkspaceMetadataResponse extends CodeWhispererTokenClient.ListWorkspaceMetadataResponse {} + +export interface DeleteWorkspaceRequest extends CodeWhispererTokenClient.DeleteWorkspaceRequest {} +export interface DeleteWorkspaceResponse extends CodeWhispererTokenClient.DeleteWorkspaceResponse {} + +export interface ListFeatureEvaluationsRequest extends CodeWhispererTokenClient.ListFeatureEvaluationsRequest {} +export interface ListFeatureEvaluationsResponse extends CodeWhispererTokenClient.ListFeatureEvaluationsResponse {} + +export interface CreateSubscriptionTokenRequest extends CodeWhispererTokenClient.CreateSubscriptionTokenRequest {} +export interface CreateSubscriptionTokenResponse extends CodeWhispererTokenClient.CreateSubscriptionTokenResponse {} + +type CodeWhispererClient = CodeWhispererSigv4Client | CodeWhispererTokenClient + +// Right now the only difference between the token client and the IAM client for codewhsiperer is the difference in function name +// This abstract class can grow in the future to account for any additional changes across the clients +export abstract class CodeWhispererServiceBase { + protected readonly codeWhispererRegion + protected readonly codeWhispererEndpoint + public shareCodeWhispererContentWithAWS = false + public customizationArn?: string + public profileArn?: string + abstract client: CodeWhispererClient + + inflightRequests: Set & RequestExtras> = new Set() + + abortInflightRequests() { + this.inflightRequests.forEach(request => { + request.abort() + }) + this.inflightRequests.clear() + } + + trackRequest(request: AWS.Request & RequestExtras) { + this.inflightRequests.add(request) + } + + completeRequest(request: AWS.Request & RequestExtras) { + this.inflightRequests.delete(request) + } + + constructor(codeWhispererRegion: string, codeWhispererEndpoint: string) { + this.codeWhispererRegion = codeWhispererRegion + this.codeWhispererEndpoint = codeWhispererEndpoint + } + + /** + * Updates Service Client options after client was instantiated. + */ + public updateClientConfig(options: ConfigurationOptions) { + this.client.config.update(options) + } + + generateItemId = () => uuidv4() + + async getSubscriptionStatus( + statusOnly?: boolean + ): Promise<{ status: 'active' | 'active-expiring' | 'none'; encodedVerificationUrl?: string }> { + // No-op/default implementation: assume no subscription + return { + status: 'none', + } + } + + async waitUntilSubscriptionActive(_cancelToken?: CancellationToken): Promise { + // No-op: base class doesn't support subscription polling + return false + } + + abstract getCredentialsType(): CredentialsType + + abstract generateSuggestions(request: GenerateSuggestionsRequest): Promise + + abstract codeModernizerCreateUploadUrl(request: CreateUploadUrlRequest): Promise + + abstract codeModernizerStartCodeTransformation( + request: StartTransformationRequest + ): Promise> + + abstract codeModernizerStopCodeTransformation( + request: StopTransformationRequest + ): Promise> + + abstract codeModernizerGetCodeTransformation( + request: GetTransformationRequest + ): Promise> + + abstract codeModernizerGetCodeTransformationPlan( + request: GetTransformationPlanRequest + ): Promise> + + abstract createUploadUrl(request: CreateUploadUrlRequest): Promise> + + abstract startCodeAnalysis( + request: StartCodeAnalysisRequest + ): Promise> + + abstract getCodeAnalysis(request: GetCodeAnalysisRequest): Promise> + + abstract listCodeAnalysisFindings( + request: ListCodeAnalysisFindingsRequest + ): Promise> + + abstract listAvailableCustomizations( + request: ListAvailableCustomizationsRequest + ): Promise> + + abstract listAvailableProfiles( + request: ListAvailableProfilesRequest + ): Promise> + + abstract sendTelemetryEvent( + request: SendTelemetryEventRequest + ): Promise> + + abstract createWorkspace(request: CreateWorkspaceRequest): Promise> + + abstract listWorkspaceMetadata( + request: ListWorkspaceMetadataRequest + ): Promise> + + abstract deleteWorkspace(request: DeleteWorkspaceRequest): Promise> + + abstract listFeatureEvaluations( + request: ListFeatureEvaluationsRequest + ): Promise> + + abstract createSubscriptionToken(request: CreateSubscriptionTokenRequest): Promise +} diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceIAM.test.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceIAM.test.ts new file mode 100644 index 0000000000..594d5dea64 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceIAM.test.ts @@ -0,0 +1,137 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + CredentialsProvider, + Workspace, + Logging, + SDKInitializator, +} from '@aws/language-server-runtimes/server-interface' +import * as sinon from 'sinon' +import * as assert from 'assert' +import { GenerateSuggestionsRequest } from './codeWhispererServiceBase' +import { CodeWhispererServiceIAM } from './codeWhispererServiceIAM' + +describe('CodeWhispererServiceIAM', function () { + let sandbox: sinon.SinonSandbox + let mockCredentialsProvider: sinon.SinonStubbedInstance + let mockWorkspace: sinon.SinonStubbedInstance + let mockLogging: sinon.SinonStubbedInstance + let mockSDKInitializator: sinon.SinonStubbedInstance + let service: CodeWhispererServiceIAM + + beforeEach(function () { + // Create the environment + sandbox = sinon.createSandbox() + + mockCredentialsProvider = { + getCredentials: sandbox.stub(), + hasCredentials: sandbox.stub(), + refresh: sandbox.stub(), + } as any + + mockWorkspace = { + getWorkspaceFolder: sandbox.stub(), + getWorkspaceFolders: sandbox.stub(), + } as any + + mockLogging = { + debug: sandbox.stub(), + error: sandbox.stub(), + info: sandbox.stub(), + warn: sandbox.stub(), + log: sandbox.stub(), + } + + mockSDKInitializator = { + initialize: sandbox.stub(), + } as any + + // Mock the createCodeWhispererSigv4Client function to avoid real client creation + const mockClient = { + generateRecommendations: sandbox.stub().returns({ + promise: sandbox.stub().resolves({ + recommendations: [], + $response: { + requestId: 'test-request-id', + httpResponse: { + headers: { 'x-amzn-sessionid': 'test-session-id' }, + }, + }, + }), + }), + setupRequestListeners: sandbox.stub(), + config: { + update: sandbox.stub(), + }, + } + + // Mock the client creation + const createClientStub = sandbox.stub( + require('../../client/sigv4/codewhisperer'), + 'createCodeWhispererSigv4Client' + ) + createClientStub.returns(mockClient) + + service = new CodeWhispererServiceIAM( + mockCredentialsProvider as any, + {} as any, // workspace parameter + mockLogging as any, + 'us-east-1', + 'https://codewhisperer.us-east-1.amazonaws.com', + mockSDKInitializator as any + ) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('getCredentialsType', function () { + it('should return iam credentials type', function () { + assert.strictEqual(service.getCredentialsType(), 'iam') + }) + }) + + describe('generateSuggestions', function () { + it('should call client.generateRecommendations and process response', async function () { + const mockRequest: GenerateSuggestionsRequest = { + fileContext: { + filename: 'test.js', + programmingLanguage: { languageName: 'javascript' }, + leftFileContent: 'const x = ', + rightFileContent: '', + }, + maxResults: 5, + } + + const result = await service.generateSuggestions(mockRequest) + + assert.strictEqual(Array.isArray(result.suggestions), true) + assert.strictEqual(typeof result.responseContext.requestId, 'string') + assert.strictEqual(typeof result.responseContext.codewhispererSessionId, 'string') + }) + + it('should add customizationArn to request if set', async function () { + service.customizationArn = 'test-arn' + + const mockRequest: GenerateSuggestionsRequest = { + fileContext: { + filename: 'test.js', + programmingLanguage: { languageName: 'javascript' }, + leftFileContent: 'const x = ', + rightFileContent: '', + }, + maxResults: 5, + } + + await service.generateSuggestions(mockRequest) + + // Verify that the client was called with the customizationArn + const clientCall = (service.client.generateRecommendations as sinon.SinonStub).getCall(0) + assert.strictEqual(clientCall.args[0].customizationArn, 'test-arn') + }) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceIAM.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceIAM.ts new file mode 100644 index 0000000000..1c3b9188bf --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceIAM.ts @@ -0,0 +1,201 @@ +import { + CredentialsProvider, + CredentialsType, + Workspace, + Logging, + SDKInitializator, +} from '@aws/language-server-runtimes/server-interface' +import { AWSError, CredentialProviderChain, Credentials } from 'aws-sdk' +import { Request } from 'aws-sdk/lib/core' +import { + CodeWhispererSigv4ClientConfigurationOptions, + createCodeWhispererSigv4Client, +} from '../../client/sigv4/codewhisperer' +import CodeWhispererSigv4Client = require('../../client/sigv4/codewhisperersigv4client') +import CodeWhispererTokenClient = require('../../client/token/codewhispererbearertokenclient') +import { + CodeWhispererServiceBase, + CreateSubscriptionTokenRequest, + CreateSubscriptionTokenResponse, + CreateUploadUrlRequest, + CreateUploadUrlResponse, + CreateWorkspaceRequest, + CreateWorkspaceResponse, + DeleteWorkspaceRequest, + DeleteWorkspaceResponse, + GenerateSuggestionsRequest, + GenerateSuggestionsResponse, + GetCodeAnalysisRequest, + GetCodeAnalysisResponse, + GetTransformationPlanRequest, + GetTransformationPlanResponse, + GetTransformationRequest, + GetTransformationResponse, + ListAvailableCustomizationsRequest, + ListAvailableCustomizationsResponse, + ListAvailableProfilesRequest, + ListAvailableProfilesResponse, + ListCodeAnalysisFindingsRequest, + ListCodeAnalysisFindingsResponse, + ListFeatureEvaluationsRequest, + ListFeatureEvaluationsResponse, + ListWorkspaceMetadataRequest, + ListWorkspaceMetadataResponse, + SendTelemetryEventRequest, + SendTelemetryEventResponse, + StartCodeAnalysisRequest, + StartCodeAnalysisResponse, + StartTransformationRequest, + StartTransformationResponse, + StopTransformationRequest, + StopTransformationResponse, + Suggestion, + SuggestionType, +} from './codeWhispererServiceBase' +import { PromiseResult } from 'aws-sdk/lib/request' + +export class CodeWhispererServiceIAM extends CodeWhispererServiceBase { + client: CodeWhispererSigv4Client + constructor( + credentialsProvider: CredentialsProvider, + workspace: Workspace, + logging: Logging, + codeWhispererRegion: string, + codeWhispererEndpoint: string, + sdkInitializator: SDKInitializator + ) { + super(codeWhispererRegion, codeWhispererEndpoint) + const options: CodeWhispererSigv4ClientConfigurationOptions = { + region: this.codeWhispererRegion, + endpoint: this.codeWhispererEndpoint, + credentialProvider: new CredentialProviderChain([ + () => credentialsProvider.getCredentials('iam') as Credentials, + ]), + } + this.client = createCodeWhispererSigv4Client(options, sdkInitializator, logging) + // Avoid overwriting any existing client listeners + const clientRequestListeners = this.client.setupRequestListeners + this.client.setupRequestListeners = (request: Request) => { + if (clientRequestListeners) { + clientRequestListeners.call(this.client, request) + } + request.httpRequest.headers['x-amzn-codewhisperer-optout'] = `${!this.shareCodeWhispererContentWithAWS}` + } + } + + getCredentialsType(): CredentialsType { + return 'iam' + } + + async generateSuggestions(request: GenerateSuggestionsRequest): Promise { + // add cancellation check + // add error check + if (this.customizationArn) request = { ...request, customizationArn: this.customizationArn } + const response = await this.client.generateRecommendations(request).promise() + const responseContext = { + requestId: response?.$response?.requestId, + codewhispererSessionId: response?.$response?.httpResponse?.headers['x-amzn-sessionid'], + nextToken: response.nextToken, + } + + for (const recommendation of response?.recommendations ?? []) { + Object.assign(recommendation, { itemId: this.generateItemId() }) + } + + return { + suggestions: response.recommendations as Suggestion[], + suggestionType: SuggestionType.COMPLETION, + responseContext, + } + } + + async codeModernizerCreateUploadUrl(request: CreateUploadUrlRequest): Promise { + throw new Error('Method not implemented.') + } + + async codeModernizerStartCodeTransformation( + request: StartTransformationRequest + ): Promise> { + throw new Error('Method not implemented.') + } + + async codeModernizerStopCodeTransformation( + request: StopTransformationRequest + ): Promise> { + throw new Error('Method not implemented.') + } + + async codeModernizerGetCodeTransformation( + request: GetTransformationRequest + ): Promise> { + throw new Error('Method not implemented.') + } + + async codeModernizerGetCodeTransformationPlan( + request: GetTransformationPlanRequest + ): Promise> { + throw new Error('Method not implemented.') + } + + async createUploadUrl(request: CreateUploadUrlRequest): Promise> { + throw new Error('Method not implemented.') + } + + async startCodeAnalysis( + request: StartCodeAnalysisRequest + ): Promise> { + throw new Error('Method not implemented.') + } + + async getCodeAnalysis(request: GetCodeAnalysisRequest): Promise> { + throw new Error('Method not implemented.') + } + + async listCodeAnalysisFindings( + request: ListCodeAnalysisFindingsRequest + ): Promise> { + throw new Error('Method not implemented.') + } + + async listAvailableCustomizations( + request: ListAvailableCustomizationsRequest + ): Promise> { + throw new Error('Method not implemented.') + } + + async listAvailableProfiles( + request: ListAvailableProfilesRequest + ): Promise> { + throw new Error('Method not implemented.') + } + + async sendTelemetryEvent( + request: SendTelemetryEventRequest + ): Promise> { + throw new Error('Method not implemented.') + } + + async createWorkspace(request: CreateWorkspaceRequest): Promise> { + throw new Error('Method not implemented.') + } + + async listWorkspaceMetadata( + request: ListWorkspaceMetadataRequest + ): Promise> { + throw new Error('Method not implemented.') + } + + async deleteWorkspace(request: DeleteWorkspaceRequest): Promise> { + throw new Error('Method not implemented.') + } + + async listFeatureEvaluations( + request: ListFeatureEvaluationsRequest + ): Promise> { + throw new Error('Method not implemented.') + } + + async createSubscriptionToken(request: CreateSubscriptionTokenRequest): Promise { + throw new Error('Method not implemented.') + } +} diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceToken.test.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceToken.test.ts new file mode 100644 index 0000000000..a1edac04e2 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceToken.test.ts @@ -0,0 +1,166 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + CredentialsProvider, + Workspace, + Logging, + SDKInitializator, +} from '@aws/language-server-runtimes/server-interface' +import * as sinon from 'sinon' +import * as assert from 'assert' +import { CodeWhispererServiceToken } from './codeWhispererServiceToken' +import { GenerateSuggestionsRequest } from './codeWhispererServiceBase' + +describe('CodeWhispererServiceToken', function () { + let sandbox: sinon.SinonSandbox + let mockCredentialsProvider: sinon.SinonStubbedInstance + let mockWorkspace: sinon.SinonStubbedInstance + let mockLogging: sinon.SinonStubbedInstance + let mockSDKInitializator: sinon.SinonStubbedInstance + let service: CodeWhispererServiceToken + let mockClient: any + + beforeEach(function () { + // Create the environment + sandbox = sinon.createSandbox() + + mockCredentialsProvider = { + getCredentials: sandbox.stub(), + hasCredentials: sandbox.stub(), + refresh: sandbox.stub(), + } as any + + mockWorkspace = { + getWorkspaceFolder: sandbox.stub(), + getWorkspaceFolders: sandbox.stub(), + } as any + + mockLogging = { + debug: sandbox.stub(), + error: sandbox.stub(), + info: sandbox.stub(), + warn: sandbox.stub(), + log: sandbox.stub(), + } + + mockSDKInitializator = { + initialize: sandbox.stub(), + } as any + + // Mock the token client + mockClient = { + generateCompletions: sandbox.stub().returns({ + promise: sandbox.stub().resolves({ + completions: [ + { + content: 'console.log("hello");', + references: [], + }, + ], + $response: { + requestId: 'test-request-id', + httpResponse: { + headers: { 'x-amzn-sessionid': 'test-session-id' }, + }, + }, + }), + }), + config: { + update: sandbox.stub(), + }, + } + + // Mock the client creation + const createTokenClientStub = sandbox.stub( + require('../../client/token/codewhisperer'), + 'createCodeWhispererTokenClient' + ) + createTokenClientStub.returns(mockClient) + + // Mock bearer credentials + mockCredentialsProvider.getCredentials.returns({ + token: 'mock-bearer-token', + }) + + service = new CodeWhispererServiceToken( + mockCredentialsProvider as any, + mockWorkspace as any, + mockLogging as any, + 'us-east-1', + 'https://codewhisperer.us-east-1.amazonaws.com', + mockSDKInitializator as any + ) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('getCredentialsType', function () { + it('should return bearer credentials type', function () { + assert.strictEqual(service.getCredentialsType(), 'bearer') + }) + }) + + describe('generateSuggestions', function () { + it('should call client.generateCompletions and process response', async function () { + const mockRequest: GenerateSuggestionsRequest = { + fileContext: { + filename: 'test.js', + programmingLanguage: { languageName: 'javascript' }, + leftFileContent: 'const x = ', + rightFileContent: '', + }, + maxResults: 5, + } + + const result = await service.generateSuggestions(mockRequest) + + assert.strictEqual(mockClient.generateCompletions.calledOnce, true) + assert.strictEqual(Array.isArray(result.suggestions), true) + assert.strictEqual(typeof result.responseContext.requestId, 'string') + assert.strictEqual(typeof result.responseContext.codewhispererSessionId, 'string') + }) + + it('should add customizationArn to request if set', async function () { + service.customizationArn = 'test-arn' + + const mockRequest: GenerateSuggestionsRequest = { + fileContext: { + filename: 'test.js', + programmingLanguage: { languageName: 'javascript' }, + leftFileContent: 'const x = ', + rightFileContent: '', + }, + maxResults: 5, + } + + await service.generateSuggestions(mockRequest) + + const clientCall = mockClient.generateCompletions.getCall(0) + assert.strictEqual(clientCall.args[0].customizationArn, 'test-arn') + }) + + it('should process profile ARN with withProfileArn method', async function () { + const mockRequest: GenerateSuggestionsRequest = { + fileContext: { + filename: 'test.js', + programmingLanguage: { languageName: 'javascript' }, + leftFileContent: 'const x = ', + rightFileContent: '', + }, + maxResults: 5, + } + + const withProfileArnStub = sandbox.stub(service, 'withProfileArn' as any) + withProfileArnStub.returns(mockRequest) + + await service.generateSuggestions(mockRequest) + + assert.strictEqual(withProfileArnStub.calledOnceWith(mockRequest), true) + }) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceToken.ts similarity index 64% rename from server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts rename to server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceToken.ts index 6ad476db05..648ac26b58 100644 --- a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService/codeWhispererServiceToken.ts @@ -9,167 +9,48 @@ import { CancellationTokenSource, } from '@aws/language-server-runtimes/server-interface' import { waitUntil } from '@aws/lsp-core/out/util/timeoutUtils' -import { AWSError, ConfigurationOptions, CredentialProviderChain, Credentials } from 'aws-sdk' +import { AWSError } from 'aws-sdk' import { PromiseResult } from 'aws-sdk/lib/request' -import { Request } from 'aws-sdk/lib/core' -import { v4 as uuidv4 } from 'uuid' -import { - CodeWhispererSigv4ClientConfigurationOptions, - createCodeWhispererSigv4Client, -} from '../client/sigv4/codewhisperer' import { CodeWhispererTokenClientConfigurationOptions, createCodeWhispererTokenClient, - RequestExtras, -} from '../client/token/codewhisperer' -import CodeWhispererSigv4Client = require('../client/sigv4/codewhisperersigv4client') -import CodeWhispererTokenClient = require('../client/token/codewhispererbearertokenclient') -import { getErrorId } from './utils' -import { GenerateCompletionsResponse } from '../client/token/codewhispererbearertokenclient' - -export interface Suggestion extends CodeWhispererTokenClient.Completion, CodeWhispererSigv4Client.Recommendation { - itemId: string -} - -export interface GenerateSuggestionsRequest extends CodeWhispererTokenClient.GenerateCompletionsRequest { - // TODO : This is broken due to Interface 'GenerateSuggestionsRequest' cannot simultaneously extend types 'GenerateCompletionsRequest' and 'GenerateRecommendationsRequest'. - //CodeWhispererSigv4Client.GenerateRecommendationsRequest { - maxResults: number -} - -export type FileContext = GenerateSuggestionsRequest['fileContext'] - -export interface ResponseContext { - requestId: string - codewhispererSessionId: string - nextToken?: string -} - -export enum SuggestionType { - EDIT = 'EDIT', - COMPLETION = 'COMPLETION', -} - -export interface GenerateSuggestionsResponse { - suggestions: Suggestion[] - suggestionType?: SuggestionType - responseContext: ResponseContext -} - -// This abstract class can grow in the future to account for any additional changes across the clients -export abstract class CodeWhispererServiceBase { - protected readonly codeWhispererRegion - protected readonly codeWhispererEndpoint - public shareCodeWhispererContentWithAWS = false - public customizationArn?: string - public profileArn?: string - abstract client: CodeWhispererSigv4Client | CodeWhispererTokenClient - - inflightRequests: Set & RequestExtras> = new Set() - - abortInflightRequests() { - this.inflightRequests.forEach(request => { - request.abort() - }) - this.inflightRequests.clear() - } - - trackRequest(request: AWS.Request & RequestExtras) { - this.inflightRequests.add(request) - } - - completeRequest(request: AWS.Request & RequestExtras) { - this.inflightRequests.delete(request) - } - - abstract getCredentialsType(): CredentialsType - - abstract generateSuggestions(request: GenerateSuggestionsRequest): Promise - - constructor(codeWhispererRegion: string, codeWhispererEndpoint: string) { - this.codeWhispererRegion = codeWhispererRegion - this.codeWhispererEndpoint = codeWhispererEndpoint - } - - /** - * Updates Service Client options after client was instantiated. - */ - public updateClientConfig(options: ConfigurationOptions) { - this.client.config.update(options) - } - - generateItemId = () => uuidv4() - - async getSubscriptionStatus( - statusOnly?: boolean - ): Promise<{ status: 'active' | 'active-expiring' | 'none'; encodedVerificationUrl?: string }> { - // No-op/default implementation: assume no subscription - return { - status: 'none', - } - } - - async waitUntilSubscriptionActive(_cancelToken?: CancellationToken): Promise { - // No-op: base class doesn't support subscription polling - return false - } -} - -export class CodeWhispererServiceIAM extends CodeWhispererServiceBase { - client: CodeWhispererSigv4Client - constructor( - credentialsProvider: CredentialsProvider, - workspace: Workspace, - logging: Logging, - codeWhispererRegion: string, - codeWhispererEndpoint: string, - sdkInitializator: SDKInitializator - ) { - super(codeWhispererRegion, codeWhispererEndpoint) - const options: CodeWhispererSigv4ClientConfigurationOptions = { - region: this.codeWhispererRegion, - endpoint: this.codeWhispererEndpoint, - credentialProvider: new CredentialProviderChain([ - () => credentialsProvider.getCredentials('iam') as Credentials, - ]), - } - this.client = createCodeWhispererSigv4Client(options, sdkInitializator, logging) - // Avoid overwriting any existing client listeners - const clientRequestListeners = this.client.setupRequestListeners - this.client.setupRequestListeners = (request: Request) => { - if (clientRequestListeners) { - clientRequestListeners.call(this.client, request) - } - request.httpRequest.headers['x-amzn-codewhisperer-optout'] = `${!this.shareCodeWhispererContentWithAWS}` - } - } - - getCredentialsType(): CredentialsType { - return 'iam' - } - - async generateSuggestions(request: GenerateSuggestionsRequest): Promise { - // add cancellation check - // add error check - if (this.customizationArn) request = { ...request, customizationArn: this.customizationArn } - const response = await this.client.generateRecommendations(request).promise() - const responseContext = { - requestId: response?.$response?.requestId, - codewhispererSessionId: response?.$response?.httpResponse?.headers['x-amzn-sessionid'], - nextToken: response.nextToken, - } - - for (const recommendation of response?.recommendations ?? []) { - Object.assign(recommendation, { itemId: this.generateItemId() }) - } - - return { - suggestions: response.recommendations as Suggestion[], - suggestionType: SuggestionType.COMPLETION, - responseContext, - } - } -} +} from '../../client/token/codewhisperer' +import CodeWhispererTokenClient = require('../../client/token/codewhispererbearertokenclient') +import { getErrorId } from '../utils' +import { GenerateCompletionsResponse } from '../../client/token/codewhispererbearertokenclient' +import { + CodeWhispererServiceBase, + CreateSubscriptionTokenRequest, + CreateSubscriptionTokenResponse, + CreateUploadUrlRequest, + CreateUploadUrlResponse, + CreateWorkspaceRequest, + DeleteWorkspaceRequest, + GenerateSuggestionsRequest, + GenerateSuggestionsResponse, + GetCodeAnalysisRequest, + GetCodeAnalysisResponse, + GetTransformationPlanRequest, + GetTransformationPlanResponse, + GetTransformationRequest, + GetTransformationResponse, + ListAvailableCustomizationsRequest, + ListAvailableProfilesRequest, + ListCodeAnalysisFindingsRequest, + ListCodeAnalysisFindingsResponse, + ListFeatureEvaluationsRequest, + ListWorkspaceMetadataRequest, + ResponseContext, + SendTelemetryEventRequest, + StartCodeAnalysisRequest, + StartCodeAnalysisResponse, + StartTransformationRequest, + StartTransformationResponse, + StopTransformationRequest, + StopTransformationResponse, + Suggestion, + SuggestionType, +} from './codeWhispererServiceBase' /** * Hint: to get an instance of this: `AmazonQTokenServiceManager.getInstance().getCodewhispererService()` @@ -177,7 +58,7 @@ export class CodeWhispererServiceIAM extends CodeWhispererServiceBase { export class CodeWhispererServiceToken extends CodeWhispererServiceBase { client: CodeWhispererTokenClient /** Debounce createSubscriptionToken by storing the current, pending promise (if any). */ - #createSubscriptionTokenPromise?: Promise + #createSubscriptionTokenPromise?: Promise /** If user clicks "Upgrade" multiple times, cancel the previous wait-promise. */ #waitUntilSubscriptionCancelSource?: CancellationTokenSource @@ -307,9 +188,7 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { } } - public async codeModernizerCreateUploadUrl( - request: CodeWhispererTokenClient.CreateUploadUrlRequest - ): Promise { + public async codeModernizerCreateUploadUrl(request: CreateUploadUrlRequest): Promise { return this.client.createUploadUrl(this.withProfileArn(request)).promise() } /** @@ -319,8 +198,8 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { */ public async codeModernizerStartCodeTransformation( - request: CodeWhispererTokenClient.StartTransformationRequest - ): Promise> { + request: StartTransformationRequest + ): Promise> { return await this.client.startTransformation(this.withProfileArn(request)).promise() } @@ -330,8 +209,8 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { * @returns transformationJobId - String id for the Job */ public async codeModernizerStopCodeTransformation( - request: CodeWhispererTokenClient.StopTransformationRequest - ): Promise> { + request: StopTransformationRequest + ): Promise> { return await this.client.stopTransformation(this.withProfileArn(request)).promise() } @@ -341,8 +220,8 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { * returns COMPLETED we know the transformation is done. */ public async codeModernizerGetCodeTransformation( - request: CodeWhispererTokenClient.GetTransformationRequest - ): Promise> { + request: GetTransformationRequest + ): Promise> { return await this.client.getTransformation(this.withProfileArn(request)).promise() } @@ -352,17 +231,15 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { * @params tranformationJobId - String id returned from StartCodeTransformationResponse */ public async codeModernizerGetCodeTransformationPlan( - request: CodeWhispererTokenClient.GetTransformationPlanRequest - ): Promise> { + request: GetTransformationPlanRequest + ): Promise> { return this.client.getTransformationPlan(this.withProfileArn(request)).promise() } /** * @description get a pre-signed url to upload source code into S3 bucket */ - async createUploadUrl( - request: CodeWhispererTokenClient.CreateUploadUrlRequest - ): Promise> { + async createUploadUrl(request: CreateUploadUrlRequest): Promise> { return this.client.createUploadUrl(this.withProfileArn(request)).promise() } @@ -370,17 +247,15 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { * @description Once source code uploaded to S3, send a request to run security scan on uploaded source code. */ async startCodeAnalysis( - request: CodeWhispererTokenClient.StartCodeAnalysisRequest - ): Promise> { + request: StartCodeAnalysisRequest + ): Promise> { return this.client.startCodeAnalysis(this.withProfileArn(request)).promise() } /** * @description Send a request to get the code scan status detail. */ - async getCodeAnalysis( - request: CodeWhispererTokenClient.GetCodeAnalysisRequest - ): Promise> { + async getCodeAnalysis(request: GetCodeAnalysisRequest): Promise> { return this.client.getCodeAnalysis(this.withProfileArn(request)).promise() } @@ -388,57 +263,57 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { * @description Once scan completed successfully, send a request to get list of all the findings for the given scan. */ async listCodeAnalysisFindings( - request: CodeWhispererTokenClient.ListCodeAnalysisFindingsRequest - ): Promise> { + request: ListCodeAnalysisFindingsRequest + ): Promise> { return this.client.listCodeAnalysisFindings(this.withProfileArn(request)).promise() } /** * @description Get list of available customizations */ - async listAvailableCustomizations(request: CodeWhispererTokenClient.ListAvailableCustomizationsRequest) { + async listAvailableCustomizations(request: ListAvailableCustomizationsRequest) { return this.client.listAvailableCustomizations(this.withProfileArn(request)).promise() } /** * @description Get list of available profiles */ - async listAvailableProfiles(request: CodeWhispererTokenClient.ListAvailableProfilesRequest) { + async listAvailableProfiles(request: ListAvailableProfilesRequest) { return this.client.listAvailableProfiles(request).promise() } /** * @description send telemetry event to code whisperer data warehouse */ - async sendTelemetryEvent(request: CodeWhispererTokenClient.SendTelemetryEventRequest) { + async sendTelemetryEvent(request: SendTelemetryEventRequest) { return this.client.sendTelemetryEvent(this.withProfileArn(request)).promise() } /** * @description create a remote workspace */ - async createWorkspace(request: CodeWhispererTokenClient.CreateWorkspaceRequest) { + async createWorkspace(request: CreateWorkspaceRequest) { return this.client.createWorkspace(this.withProfileArn(request)).promise() } /** * @description get list of workspace metadata */ - async listWorkspaceMetadata(request: CodeWhispererTokenClient.ListWorkspaceMetadataRequest) { + async listWorkspaceMetadata(request: ListWorkspaceMetadataRequest) { return this.client.listWorkspaceMetadata(this.withProfileArn(request)).promise() } /** * @description delete the remote workspace */ - async deleteWorkspace(request: CodeWhispererTokenClient.DeleteWorkspaceRequest) { + async deleteWorkspace(request: DeleteWorkspaceRequest) { return this.client.deleteWorkspace(this.withProfileArn(request)).promise() } /* * @description get the list of feature evaluations */ - async listFeatureEvaluations(request: CodeWhispererTokenClient.ListFeatureEvaluationsRequest) { + async listFeatureEvaluations(request: ListFeatureEvaluationsRequest) { return this.client.listFeatureEvaluations(this.withProfileArn(request)).promise() } @@ -447,7 +322,7 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { * * cool api you have there 🥹 */ - async createSubscriptionToken(request: CodeWhispererTokenClient.CreateSubscriptionTokenRequest) { + async createSubscriptionToken(request: CreateSubscriptionTokenRequest) { // Debounce. if (this.#createSubscriptionTokenPromise) { return this.#createSubscriptionTokenPromise diff --git a/server/aws-lsp-codewhisperer/src/shared/streamingClientService.ts b/server/aws-lsp-codewhisperer/src/shared/streamingClientService.ts index cc0ec57070..7105e192fa 100644 --- a/server/aws-lsp-codewhisperer/src/shared/streamingClientService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/streamingClientService.ts @@ -34,6 +34,7 @@ export type ChatCommandOutput = SendMessageCommandOutput | GenerateAssistantResp export abstract class StreamingClientServiceBase { protected readonly region protected readonly endpoint + public profileArn?: string inflightRequests: Set = new Set() @@ -49,6 +50,16 @@ export abstract class StreamingClientServiceBase { abortController?: AbortController ): Promise + abstract generateAssistantResponse( + request: GenerateAssistantResponseCommandInputCodeWhispererStreaming, + abortController?: AbortController + ): Promise + + abstract exportResultArchive( + request: ExportResultArchiveCommandInputCodeWhispererStreaming, + abortController?: AbortController + ): Promise + public abortInflightRequests() { this.inflightRequests.forEach(abortController => { abortController.abort() @@ -59,7 +70,6 @@ export abstract class StreamingClientServiceBase { export class StreamingClientServiceToken extends StreamingClientServiceBase { client: CodeWhispererStreaming - public profileArn?: string constructor( credentialsProvider: CredentialsProvider, sdkInitializator: SDKInitializator, @@ -209,4 +219,17 @@ export class StreamingClientServiceIAM extends StreamingClientServiceBase { return response } + + public async generateAssistantResponse( + request: GenerateAssistantResponseCommandInputCodeWhispererStreaming, + abortController?: AbortController + ): Promise { + throw new Error('Method not implemented.') + } + public async exportResultArchive( + request: ExportResultArchiveCommandInputCodeWhispererStreaming, + abortController?: AbortController + ): Promise { + throw new Error('Method not implemented.') + } } diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetry.test.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetry.test.ts index 86b9e4ec9d..e4d513caea 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetry.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetry.test.ts @@ -9,7 +9,12 @@ import { TestFeatures } from '@aws/language-server-runtimes/testing' import sinon, { StubbedInstance, stubInterface } from 'ts-sinon' import { TextDocument } from 'vscode-languageserver-textdocument' import { CodewhispererServerFactory } from '../../language-server/inline-completion/codeWhispererServer' -import { CodeWhispererServiceBase, ResponseContext, Suggestion, SuggestionType } from '../codeWhispererService' +import { + CodeWhispererServiceBase, + ResponseContext, + Suggestion, + SuggestionType, +} from '../codeWhispererService/codeWhispererServiceBase' import { TelemetryService } from './telemetryService' import { initBaseTestServiceManager, TestAmazonQServiceManager } from '../amazonQServiceManager/testUtils' diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts index 14df60adb9..97f917156c 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts @@ -15,7 +15,7 @@ import { CodeWhispererSession } from '../../language-server/inline-completion/se import sinon, { StubbedInstance, stubInterface } from 'ts-sinon' import { BUILDER_ID_START_URL } from '../constants' import { ChatInteractionType } from './types' -import { CodeWhispererServiceToken } from '../codeWhispererService' +import { CodeWhispererServiceToken } from '../codeWhispererService/codeWhispererServiceToken' import { initBaseTestServiceManager, TestAmazonQServiceManager } from '../amazonQServiceManager/testUtils' import { TestFeatures } from '@aws/language-server-runtimes/testing' diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts index 5f7a806427..a27dd6da1b 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts @@ -1,4 +1,5 @@ -import { CodeWhispererServiceToken, SuggestionType } from '../codeWhispererService' +import { CodeWhispererServiceToken } from '../codeWhispererService/codeWhispererServiceToken' +import { SuggestionType } from '../codeWhispererService/codeWhispererServiceBase' import { CredentialsProvider, CredentialsType, diff --git a/server/aws-lsp-codewhisperer/src/shared/testUtils.ts b/server/aws-lsp-codewhisperer/src/shared/testUtils.ts index 38d6e960cb..79596d8cc6 100644 --- a/server/aws-lsp-codewhisperer/src/shared/testUtils.ts +++ b/server/aws-lsp-codewhisperer/src/shared/testUtils.ts @@ -1,5 +1,5 @@ import { TextDocument } from 'vscode-languageserver-textdocument' -import { CodeWhispererServiceBase, ResponseContext, Suggestion } from './codeWhispererService' +import { CodeWhispererServiceBase, ResponseContext, Suggestion } from './codeWhispererService/codeWhispererServiceBase' import { TestFeatures } from '@aws/language-server-runtimes/testing' import { SsoConnectionType } from './utils' import { stubInterface } from 'ts-sinon' diff --git a/server/aws-lsp-codewhisperer/src/shared/utils.ts b/server/aws-lsp-codewhisperer/src/shared/utils.ts index 3105b3a79a..12ed3008ab 100644 --- a/server/aws-lsp-codewhisperer/src/shared/utils.ts +++ b/server/aws-lsp-codewhisperer/src/shared/utils.ts @@ -6,7 +6,7 @@ import { } from '@aws/language-server-runtimes/server-interface' import { AWSError, Credentials } from 'aws-sdk' import { distance } from 'fastest-levenshtein' -import { Suggestion } from './codeWhispererService' +import { Suggestion } from './codeWhispererService/codeWhispererServiceBase' import { CodewhispererCompletionType } from './telemetry/types' import { BUILDER_ID_START_URL, diff --git a/server/aws-lsp-identity/src/iam/iamProvider.test.ts b/server/aws-lsp-identity/src/iam/iamProvider.test.ts new file mode 100644 index 0000000000..19b0d5ff43 --- /dev/null +++ b/server/aws-lsp-identity/src/iam/iamProvider.test.ts @@ -0,0 +1,401 @@ +import { expect, use } from 'chai' +import { StubbedInstance, stubInterface } from 'ts-sinon' +import { ProfileData, ProfileStore } from '../language-server/profiles/profileService' +import { createStubInstance, restore, SinonSpy, SinonStub, spy, stub } from 'sinon' +import { CancellationToken, Profile, ProfileKind } from '@aws/language-server-runtimes/protocol' +import { Logging, Telemetry } from '@aws/language-server-runtimes/server-interface' +import { IamCredentials, Observability } from '@aws/lsp-core' +import { StsCache } from '../sts/cache/stsCache' +import { StsAutoRefresher } from '../sts/stsAutoRefresher' +import { IamProvider } from '../iam/iamProvider' +import { IamFlowParams } from './utils' +import * as iamUtils from '../iam/utils' +import { STSClient } from '@aws-sdk/client-sts' + +// eslint-disable-next-line +use(require('chai-as-promised')) + +let sut: IamProvider +let defaultParams: IamFlowParams +let defaultProfile: Profile +let profileStore: StubbedInstance +let stsCache: StubbedInstance +let stsAutoRefresher: StubbedInstance +let handlers: StubbedInstance +let observability: StubbedInstance +let token: StubbedInstance +let checkMfaRequiredStub: SinonStub< + [credentials: IamCredentials, permissions: string[], region?: string | undefined], + Promise +> +let providerSpy: SinonSpy +let emitMetricSpy: SinonSpy + +describe('IamProvider', () => { + beforeEach(() => { + defaultProfile = { + kinds: [ProfileKind.Unknown], + name: 'default-profile', + } + + profileStore = stubInterface({ + load: Promise.resolve({ + profiles: [ + { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'cyclic-profile-1', + settings: { + role_arn: 'my-role-arn', + source_profile: 'cyclic-profile-1', + }, + }, + { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'cyclic-profile-2', + settings: { + role_arn: 'my-role-arn', + source_profile: 'cyclic-profile-3', + }, + }, + { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'cyclic-profile-3', + settings: { + role_arn: 'my-role-arn', + source_profile: 'cyclic-profile-2', + }, + }, + { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'base-profile', + settings: { + role_arn: 'my-role-arn', + source_profile: 'intermediate-profile', + }, + }, + { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'intermediate-profile', + settings: { + role_arn: 'my-role-arn', + source_profile: 'my-iam-profile', + }, + }, + { + kinds: [ProfileKind.IamCredentialsProfile], + name: 'my-iam-profile', + settings: { + aws_access_key_id: 'my-access-key', + aws_secret_access_key: 'my-secret-key', + }, + }, + ], + ssoSessions: [], + } satisfies ProfileData), + }) + + stsCache = stubInterface({ + getStsCredential: Promise.resolve(undefined), + setStsCredential: Promise.resolve(), + removeStsCredential: Promise.resolve(), + }) + + stsAutoRefresher = createStubInstance(StsAutoRefresher, { + watch: Promise.resolve(), + unwatch: undefined, + }) as StubbedInstance + + observability = stubInterface() + observability.logging = stubInterface() + observability.telemetry = stubInterface() + + handlers = stubInterface({ + sendGetMfaCode: Promise.resolve({ code: 'mfa-code', mfaSerial: 'mfa-serial' }), + }) + + providerSpy = spy(() => () => { + return { + accessKeyId: 'provider-access-key', + secretAccessKey: 'provider-secret-key', + sessionToken: 'provider-session-token', + credentialScope: 'provider-credential-scope', + accountId: 'provider-account-id', + } + }) + + emitMetricSpy = spy() + + token = stubInterface() + + defaultParams = { + profile: defaultProfile, + callStsOnInvalidIamCredential: true, + recursionCount: 0, + profileStore: profileStore, + stsCache: stsCache, + stsAutoRefresher: stsAutoRefresher, + handlers: handlers, + providers: { + fromProcess: providerSpy, + fromEnv: providerSpy, + fromInstanceMetadata: providerSpy, + fromContainerMetadata: providerSpy, + }, + emitMetric: emitMetricSpy, + token: token, + observability: observability, + } + + sut = new IamProvider() + + checkMfaRequiredStub = stub(iamUtils, 'checkMfaRequired') + checkMfaRequiredStub.resolves(false) + + stub(STSClient.prototype, 'send').resolves({ + Credentials: { + AccessKeyId: 'role-access-key', + SecretAccessKey: 'role-secret-key', + SessionToken: 'role-session-token', + Expiration: new Date('2024-09-25T18:09:20.455Z'), + }, + AssumedRoleUser: { + Arn: 'role-arn', + AssumedRoleId: 'role-id', + }, + }) + }) + + afterEach(() => { + restore() + }) + + describe('getCredential', () => { + it('Can get credentials from profile', async () => { + const profile: Profile = { + kinds: [ProfileKind.IamCredentialsProfile], + name: 'iam-profile', + settings: { + aws_access_key_id: 'access-key', + aws_secret_access_key: 'secret-key', + aws_session_token: 'session-token', + }, + } + const actual = await sut.getCredential({ ...defaultParams, profile: profile }) + + expect(actual.credentials.accessKeyId).to.equal('access-key') + expect(actual.credentials.secretAccessKey).to.equal('secret-key') + expect(actual.credentials.sessionToken).to.equal('session-token') + expect(emitMetricSpy.calledWith('Succeeded', null, 'staticSessionProfile')).to.be.true + }) + + it('Can generate credentials by assuming role.', async () => { + const profile: Profile = { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'my-role-profile', + settings: { + role_arn: 'my-role-arn', + source_profile: 'my-iam-profile', + }, + } + const actual = await sut.getCredential({ ...defaultParams, profile: profile }) + + expect(actual.credentials.accessKeyId).to.equal('role-access-key') + expect(actual.credentials.secretAccessKey).to.equal('role-secret-key') + expect(actual.credentials.sessionToken).to.equal('role-session-token') + expect(actual.credentials.expiration?.toISOString()).to.equal('2024-09-25T18:09:20.455Z') + expect(stsAutoRefresher.watch.calledOnce).to.be.true + expect(emitMetricSpy.calledWith('Succeeded', null, 'assumeRoleProfile')).to.be.true + }) + + it('Can generate credentials with MFA.', async () => { + checkMfaRequiredStub.resolves(true) + const profile: Profile = { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'my-mfa-profile', + settings: { + role_arn: 'my-role-arn', + source_profile: 'my-iam-profile', + mfa_serial: 'my-device-arn', + }, + } + const actual = await sut.getCredential({ ...defaultParams, profile: profile }) + + expect(actual.credentials.accessKeyId).to.equal('role-access-key') + expect(actual.credentials.secretAccessKey).to.equal('role-secret-key') + expect(actual.credentials.sessionToken).to.equal('role-session-token') + expect(actual.credentials.expiration?.toISOString()).to.equal('2024-09-25T18:09:20.455Z') + expect(handlers.sendGetMfaCode.calledOnce).to.be.true + expect(emitMetricSpy.calledWith('Succeeded', null, 'assumeMfaRoleProfile')).to.be.true + }) + + it('Returns existing STS credential.', async () => { + const profile: Profile = { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'my-role-profile', + settings: { + role_arn: 'my-role-arn', + source_profile: 'my-iam-profile', + }, + } + stsCache.getStsCredential = (() => + Promise.resolve({ + accessKeyId: 'other-access-key', + secretAccessKey: 'other-secret-key', + sessionToken: 'other-session-token', + expiration: new Date('2024-10-25T18:09:20.455Z'), + })) as any + const actual = await sut.getCredential({ ...defaultParams, profile: profile }) + + expect(actual.credentials.accessKeyId).to.equal('other-access-key') + expect(actual.credentials.secretAccessKey).to.equal('other-secret-key') + expect(actual.credentials.sessionToken).to.equal('other-session-token') + expect(actual.credentials.expiration?.toISOString()).to.equal('2024-10-25T18:09:20.455Z') + expect(stsAutoRefresher.watch.calledOnce).to.be.true + expect(emitMetricSpy.calledWith('Succeeded', null, 'assumeRoleProfile')).to.be.true + }) + + it('Throws when no STS credential cached and callStsOnInvalidIamCredential is false.', async () => { + const profile: Profile = { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'my-role-profile', + settings: { + role_arn: 'my-role-arn', + source_profile: 'my-iam-profile', + }, + } + const error = await expect( + sut.getCredential({ ...defaultParams, profile: profile, callStsOnInvalidIamCredential: false }) + ).rejectedWith(Error) + + expect(error.message).to.equal('STS credential not found.') + expect(stsAutoRefresher.watch.calledOnce).to.be.false + }) + + it('Can login with chained IamSourceProfileProfiles.', async () => { + const profile: Profile = { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'base-profile', + settings: { + role_arn: 'my-role-arn', + source_profile: 'intermediate-profile', + }, + } + const actual = await sut.getCredential({ ...defaultParams, profile: profile }) + + expect(actual.credentials.accessKeyId).to.equal('role-access-key') + expect(actual.credentials.secretAccessKey).to.equal('role-secret-key') + expect(actual.credentials.sessionToken).to.equal('role-session-token') + expect(actual.credentials.expiration?.toISOString()).to.equal('2024-09-25T18:09:20.455Z') + expect(stsAutoRefresher.watch.called).to.be.true + expect(emitMetricSpy.calledWith('Succeeded', null, 'assumeRoleProfile')).to.be.true + }) + + it('Throws when IamSourceProfileProfile points to itself.', async () => { + const profile: Profile = { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'cyclic-profile-1', + settings: { + role_arn: 'my-role-arn', + source_profile: 'cyclic-profile-1', + }, + } + const error = await expect(sut.getCredential({ ...defaultParams, profile: profile })).rejectedWith(Error) + + expect(error.message).to.equal('Source profile chain exceeded max length.') + expect(stsAutoRefresher.watch.calledOnce).to.be.false + }) + + it('Throws when IamSourceProfileProfile form cycle.', async () => { + const profile: Profile = { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'cyclic-profile-2', + settings: { + role_arn: 'my-role-arn', + source_profile: 'cyclic-profile-3', + }, + } + const error = await expect(sut.getCredential({ ...defaultParams, profile: profile })).rejectedWith(Error) + + expect(error.message).to.equal('Source profile chain exceeded max length.') + expect(stsAutoRefresher.watch.calledOnce).to.be.false + }) + + it('Can login with credential process.', async () => { + const profile: Profile = { + kinds: [ProfileKind.IamCredentialProcessProfile], + name: 'process-profile', + settings: { + role_arn: 'my-role-arn', + credential_process: 'my-process', + }, + } + const actual = await sut.getCredential({ ...defaultParams, profile: profile }) + + expect(actual.credentials.accessKeyId).to.equal('provider-access-key') + expect(actual.credentials.secretAccessKey).to.equal('provider-secret-key') + expect(actual.credentials.sessionToken).to.equal('provider-session-token') + expect(providerSpy.calledOnce).to.be.true + expect(emitMetricSpy.calledWith('Succeeded', null, 'credentialProcessProfile')).to.be.true + }) + + it('Can assume role with environment variables', async () => { + const profile: Profile = { + kinds: [ProfileKind.IamCredentialSourceProfile], + name: 'env-profile', + settings: { + role_arn: 'my-role-arn', + credential_source: 'Environment', + }, + } + const actual = await sut.getCredential({ ...defaultParams, profile: profile }) + + expect(actual.credentials.accessKeyId).to.equal('role-access-key') + expect(actual.credentials.secretAccessKey).to.equal('role-secret-key') + expect(actual.credentials.sessionToken).to.equal('role-session-token') + expect(actual.credentials.expiration?.toISOString()).to.equal('2024-09-25T18:09:20.455Z') + expect(providerSpy.calledOnce).to.be.true + expect(stsAutoRefresher.watch.calledOnce).to.be.true + expect(emitMetricSpy.calledWith('Succeeded', null, 'environment')).to.be.true + }) + + it('Can assume role with EC2 metadata', async () => { + const profile: Profile = { + kinds: [ProfileKind.IamCredentialSourceProfile], + name: 'env-profile', + settings: { + role_arn: 'my-role-arn', + credential_source: 'Ec2InstanceMetadata', + }, + } + const actual = await sut.getCredential({ ...defaultParams, profile: profile }) + + expect(actual.credentials.accessKeyId).to.equal('role-access-key') + expect(actual.credentials.secretAccessKey).to.equal('role-secret-key') + expect(actual.credentials.sessionToken).to.equal('role-session-token') + expect(actual.credentials.expiration?.toISOString()).to.equal('2024-09-25T18:09:20.455Z') + expect(providerSpy.calledOnce).to.be.true + expect(stsAutoRefresher.watch.calledOnce).to.be.true + expect(emitMetricSpy.calledWith('Succeeded', null, 'ec2Metadata')).to.be.true + }) + + it('Can assume role with ECS metadata', async () => { + const profile: Profile = { + kinds: [ProfileKind.IamCredentialSourceProfile], + name: 'env-profile', + settings: { + role_arn: 'my-role-arn', + credential_source: 'EcsContainer', + }, + } + const actual = await sut.getCredential({ ...defaultParams, profile: profile }) + + expect(actual.credentials.accessKeyId).to.equal('role-access-key') + expect(actual.credentials.secretAccessKey).to.equal('role-secret-key') + expect(actual.credentials.sessionToken).to.equal('role-session-token') + expect(actual.credentials.expiration?.toISOString()).to.equal('2024-09-25T18:09:20.455Z') + expect(providerSpy.calledOnce).to.be.true + expect(stsAutoRefresher.watch.calledOnce).to.be.true + expect(emitMetricSpy.calledWith('Succeeded', null, 'ecsMetatdata')).to.be.true + }) + }) +}) diff --git a/server/aws-lsp-identity/src/iam/iamProvider.ts b/server/aws-lsp-identity/src/iam/iamProvider.ts new file mode 100644 index 0000000000..816a3f7ed5 --- /dev/null +++ b/server/aws-lsp-identity/src/iam/iamProvider.ts @@ -0,0 +1,223 @@ +import { + AwsErrorCodes, + GetMfaCodeResult, + IamCredential, + IamCredentials, + ProfileKind, +} from '@aws/language-server-runtimes/server-interface' +import { AwsError } from '@aws/lsp-core' +import { AssumeRoleCommand, AssumeRoleCommandInput, STSClient } from '@aws-sdk/client-sts' +import { checkMfaRequired, IamFlowParams } from './utils' +import { convertProfileToId } from '../sts/cache/fileSystemStsCache' + +const sourceProfileRecursionMax = 5 + +export class IamProvider { + readonly defaultRegion = 'us-east-1' + + async getCredential(params: IamFlowParams): Promise { + let credentials: IamCredentials + + // Get the credentials directly from the profile + if (params.profile.kinds.includes(ProfileKind.IamCredentialsProfile)) { + credentials = { + accessKeyId: params.profile.settings!.aws_access_key_id!, + secretAccessKey: params.profile.settings!.aws_secret_access_key!, + sessionToken: params.profile.settings?.aws_session_token, + } + params.emitMetric('Succeeded', null, credentials.sessionToken ? 'staticSessionProfile' : 'staticProfile') + } + // Assume the role matching the found ARN + else if ( + params.profile.kinds.includes(ProfileKind.IamSourceProfileProfile) || + params.profile.kinds.includes(ProfileKind.IamCredentialSourceProfile) + ) { + credentials = await this.getAssumedRoleCredential(params) + } + // Get the credentials from the process output + else if (params.profile.kinds.includes(ProfileKind.IamCredentialProcessProfile)) { + credentials = await params.providers.fromProcess({ profile: params.profile.name })() + params.emitMetric('Succeeded', null, 'credentialProcessProfile') + } else { + throw new AwsError( + 'Credentials could not be found for provided profile kind', + AwsErrorCodes.E_INVALID_PROFILE + ) + } + + return { id: convertProfileToId(params.profile), kinds: params.profile.kinds, credentials: credentials } + } + + private async getAssumedRoleCredential(params: IamFlowParams): Promise { + if (!params.profile.settings) { + throw new AwsError('Profile settings not found when assuming role.', AwsErrorCodes.E_INVALID_PROFILE) + } + + // Try to get the STS credentials from cache + let result: IamCredentials + const credential = await params.stsCache + .getStsCredential(convertProfileToId(params.profile)) + .catch(_ => undefined) + + if (credential) { + result = credential + params.emitMetric('Succeeded', null, 'assumeRoleProfile') + } else if (params.callStsOnInvalidIamCredential) { + // Generate STS credentials + result = await this.generateStsCredential(params) + // Cache STS credentials + await params.stsCache.setStsCredential(convertProfileToId(params.profile), result) + } else { + // If we could not get the cached STS credential and cannot generate a new credential, give up + params.observability.logging.log( + 'STS credential not found an callStsOnInvalidIamCredential = false, returning no credential.' + ) + throw new AwsError('STS credential not found.', AwsErrorCodes.E_INVALID_STS_CREDENTIAL) + } + + // Set up auto-refresh if MFA is disabled + if (!params.profile.settings.mfa_serial) { + await params.stsAutoRefresher + .watch(convertProfileToId(params.profile), () => this.generateStsCredential(params)) + .catch(reason => { + params.observability.logging.log(`Unable to auto-refresh STS credentials. ${reason}`) + }) + } + + return result + } + + private async getParentCredential(params: IamFlowParams): Promise { + let parentCredentials: IamCredentials + if (params.profile.kinds.includes(ProfileKind.IamSourceProfileProfile)) { + // Get the source profile + const profileData = await params.profileStore.load() + const sourceName = params.profile.settings!.source_profile! + const sourceProfile = profileData.profiles.find(p => p.name === sourceName) + if (!sourceProfile) { + params.observability.logging.log(`Source profile ${sourceName} not found.`) + throw new AwsError(`Source profile ${sourceName} not found.`, AwsErrorCodes.E_PROFILE_NOT_FOUND) + } + // Obtain parent profile credentials if IamRoleSourceProfile chain isn't too long + if (params.recursionCount <= sourceProfileRecursionMax) { + const response = await this.getCredential({ + ...params, + profile: sourceProfile, + recursionCount: params.recursionCount + 1, + }) + parentCredentials = response.credentials + } else { + throw new AwsError('Source profile chain exceeded max length.', AwsErrorCodes.E_INVALID_PROFILE) + } + } else if (params.profile.kinds.includes(ProfileKind.IamCredentialSourceProfile)) { + switch (params.profile.settings?.credential_source) { + // TODO: test whether EC2 and ECS metadata credentials are retrieved as expected + case 'Ec2InstanceMetadata': + parentCredentials = await params.providers.fromInstanceMetadata()() + params.emitMetric('Succeeded', null, 'ec2Metadata') + break + case 'EcsContainer': + parentCredentials = await params.providers.fromContainerMetadata()() + params.emitMetric('Succeeded', null, 'ecsMetatdata') + break + case 'Environment': + parentCredentials = await params.providers.fromEnv()() + params.emitMetric('Succeeded', null, 'environment') + break + default: + throw new AwsError( + `Unsupported credential source: ${params.profile.settings?.credential_source}`, + AwsErrorCodes.E_INVALID_PROFILE + ) + } + } else { + throw new AwsError('Source credentials not found', AwsErrorCodes.E_INVALID_PROFILE) + } + return parentCredentials + } + + private async generateStsCredential(params: IamFlowParams): Promise { + try { + // Set up AssumeRole input + const parentCredentials = await this.getParentCredential(params) + const stsClient = new STSClient({ + region: params.profile.settings?.region || this.defaultRegion, + credentials: parentCredentials, + }) + const assumeRoleInput: AssumeRoleCommandInput = { + RoleArn: params.profile.settings?.role_arn, + RoleSessionName: params.profile.settings?.role_session_name || `session-${Date.now()}`, + DurationSeconds: 3600, + } + + // Add MFA fields to assume role request if MultiFactorAuthPresent is required + const mfaRequired = await checkMfaRequired( + parentCredentials, + ['sts:AssumeRole'], + params.profile.settings?.region + ) + if (mfaRequired) { + const response = await this.requestMfa(params) + assumeRoleInput.SerialNumber = response.mfaSerial + assumeRoleInput.TokenCode = response.code + + // Add the MFA serial number to the profile + const updatedProfile = { + ...params.profile, + settings: { ...params.profile.settings, mfa_serial: response.mfaSerial }, + } + params.profileStore.save({ profiles: [updatedProfile], ssoSessions: [] }) + // Update params.profile to ensure STS cache key is generated with updated MFA serial number + params.profile = updatedProfile + } + + // Call AssumeRole API + const command = new AssumeRoleCommand(assumeRoleInput) + const { Credentials } = await stsClient.send(command) + if (!Credentials?.AccessKeyId || !Credentials.SecretAccessKey) { + throw new AwsError( + 'Failed to generate credentials for assumed role', + AwsErrorCodes.E_CANNOT_CREATE_STS_CREDENTIAL + ) + } + + // Emit metric if this is not an intermediate profile + if (params.recursionCount === 0) { + params.emitMetric('Succeeded', null, mfaRequired ? 'assumeMfaRoleProfile' : 'assumeRoleProfile') + } + + return { + accessKeyId: Credentials.AccessKeyId, + secretAccessKey: Credentials.SecretAccessKey, + sessionToken: Credentials.SessionToken, + expiration: Credentials.Expiration, + } + } catch (e) { + params.observability.logging.log(`Error generating STS credentials.`) + throw e + } + } + + // Request an MFA code from the language client + private async requestMfa(params: IamFlowParams): Promise { + const response = await params.handlers.sendGetMfaCode({ + profileName: params.profile.name, + mfaSerial: params.profile.settings?.mfa_serial, + }) + + if (!response.code) { + throw new AwsError( + 'MFA code required when assuming role with MultiFactorAuthPresent permission condition', + AwsErrorCodes.E_MFA_REQUIRED + ) + } + if (!response.mfaSerial) { + throw new AwsError( + 'MFA serial required when assuming role with MultiFactorAuthPresent permission condition', + AwsErrorCodes.E_MFA_REQUIRED + ) + } + + return response + } +} diff --git a/server/aws-lsp-identity/src/iam/utils.ts b/server/aws-lsp-identity/src/iam/utils.ts new file mode 100644 index 0000000000..e331b1c2be --- /dev/null +++ b/server/aws-lsp-identity/src/iam/utils.ts @@ -0,0 +1,135 @@ +import { IAMClient, SimulatePrincipalPolicyCommand, SimulatePrincipalPolicyCommandOutput } from '@aws-sdk/client-iam' +import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts' +import { + AwsErrorCodes, + CancellationToken, + GetMfaCodeParams, + GetMfaCodeResult, + IamCredentials, + Profile, + ProfileChangedParams, + StsCredentialChangedParams, +} from '@aws/language-server-runtimes/server-interface' +import { AwsError, Observability } from '@aws/lsp-core' +import { StsCache } from '../sts/cache/stsCache' +import { StsAutoRefresher } from '../sts/stsAutoRefresher' +import { ProfileStore } from '../language-server/profiles/profileService' +import { FromProcessInit } from '@aws-sdk/credential-provider-process' +import { AwsCredentialIdentityProvider, Provider, RuntimeConfigAwsCredentialIdentityProvider } from '@aws-sdk/types' +import { InstanceMetadataCredentials, RemoteProviderInit } from '@smithy/credential-provider-imds' +import { FromEnvInit } from '@aws-sdk/credential-provider-env' + +const defaultRegion = 'us-east-1' + +export async function validatePermissions( + credentials: IamCredentials, + permissions: string[], + region?: string +): Promise { + const response = await simulatePermissions(credentials, permissions, region) + // If evaluation results are missing, assume caller does not have sufficient permissions + if (!response.EvaluationResults) { + return false + } + return response.EvaluationResults.every(result => result.EvalDecision === 'allowed') +} + +export async function checkMfaRequired( + credentials: IamCredentials, + permissions: string[], + region?: string +): Promise { + const response = await simulatePermissions(credentials, permissions, region) + // If evaluation results are missing, assume caller does not need MFA + if (!response.EvaluationResults) { + return false + } + return response.EvaluationResults?.some(result => + result?.MissingContextValues?.includes('aws:MultiFactorAuthPresent') + ) +} + +export function throwOnInvalidCredentialId(iamCredentialId?: string): asserts iamCredentialId is string { + if (typeof iamCredentialId?.trim !== 'function' || !iamCredentialId?.trim()) { + throw new AwsError('IAM credential id is invalid.', AwsErrorCodes.E_INVALID_STS_CREDENTIAL) + } +} + +// Simulate permissions on the identity associated with the credentials +async function simulatePermissions( + credentials: IamCredentials, + permissions: string[], + region?: string +): Promise { + // Convert the credentials into an identity + const stsClient = new STSClient({ region: region || defaultRegion, credentials: credentials }) + const identity = await stsClient.send(new GetCallerIdentityCommand({})) + if (!identity.Arn) { + throw new AwsError('Caller identity ARN not found.', AwsErrorCodes.E_CALLER_IDENTITY_NOT_FOUND) + } + + // Simulate permissions on the identity + const iamClient = new IAMClient({ region: region || defaultRegion, credentials: credentials }) + return await iamClient.send( + new SimulatePrincipalPolicyCommand({ + PolicySourceArn: convertToIamArn(identity.Arn), + ActionNames: permissions, + }) + ) +} + +// Converts an assumed role ARN into an IAM role ARN +function convertToIamArn(arn: string) { + if (arn.includes(':assumed-role/')) { + const parts = arn.split(':') + const roleName = parts[5].split('/')[1] + return `arn:aws:iam::${parts[4]}:role/${roleName}` + } else { + return arn + } +} + +export type CredentialProviders = { + fromProcess: (init?: FromProcessInit) => RuntimeConfigAwsCredentialIdentityProvider + fromContainerMetadata: (init?: RemoteProviderInit) => AwsCredentialIdentityProvider + fromInstanceMetadata: (init?: RemoteProviderInit) => Provider + fromEnv: (init?: FromEnvInit) => AwsCredentialIdentityProvider +} + +export type SendGetMfaCode = (params: GetMfaCodeParams) => Promise +export type SendStsCredentialChanged = (params: StsCredentialChangedParams) => void +export type SendProfileChanged = (params: ProfileChangedParams) => void + +export type IamHandlers = { + sendGetMfaCode: SendGetMfaCode +} + +// Based on: +// https://github.com/aws/aws-toolkit-common/blob/e57f13e15adcd4e8c7c43313431f0a134ff804eb/telemetry/definitions/commonDefinitions.json +type EmitMetric = ( + result: 'Succeeded' | 'Failed' | 'Cancelled', + error?: unknown, + credentialType?: + | 'staticProfile' + | 'staticSessionProfile' + | 'credentialProcessProfile' + | 'assumeRoleProfile' + | 'assumeMfaRoleProfile' + | 'ecsMetatdata' + | 'ec2Metadata' + | 'environment' +) => void + +export type IamFlowParams = { + profile: Profile + callStsOnInvalidIamCredential: boolean + recursionCount: number + profileStore: ProfileStore + stsCache: StsCache + stsAutoRefresher: StsAutoRefresher + handlers: IamHandlers + providers: CredentialProviders + token: CancellationToken + emitMetric: EmitMetric + observability: Observability +} diff --git a/server/aws-lsp-identity/src/language-server/autoRefresher.ts b/server/aws-lsp-identity/src/language-server/autoRefresher.ts new file mode 100644 index 0000000000..7f7f72f3f1 --- /dev/null +++ b/server/aws-lsp-identity/src/language-server/autoRefresher.ts @@ -0,0 +1,46 @@ +import { Observability } from '@aws/lsp-core' + +export const invalidDelay: number = -1 +export const refreshWindowMillis: number = 5 * 60 * 1000 +export const retryCooldownWindowMillis: number = 30000 +const bufferedRefreshWindowMillis = refreshWindowMillis * 0.95 +const bufferedRetryCooldownWindowMillis = retryCooldownWindowMillis * 1.05 +const maxRefreshJitterMillis = 10000 +const maxRetryCooldownJitterMillis = 3000 + +export abstract class AutoRefresher implements Disposable { + protected readonly timeouts: Record = {} + + constructor(protected readonly observability: Observability) {} + + [Symbol.dispose](): void { + for (const key of Object.keys(this.timeouts)) { + this.unwatch(key) + } + } + + protected abstract unwatch(key: string): void + + getDelay(expiration: string): number { + const nowMillis = Date.now() + const expiresAtMillis = Date.parse(expiration) + let delayMillis: number + + if (nowMillis < expiresAtMillis - refreshWindowMillis) { + // Before refresh window, schedule to run in refresh window with jitter + delayMillis = expiresAtMillis - bufferedRefreshWindowMillis - nowMillis + delayMillis += Math.random() * maxRefreshJitterMillis // Jitter to mitigate race conditions + } else if (expiresAtMillis - refreshWindowMillis < nowMillis && nowMillis < expiresAtMillis) { + // In refresh window with time for a retry + delayMillis = bufferedRetryCooldownWindowMillis + delayMillis += Math.random() * maxRetryCooldownJitterMillis // Jitter to mitigate race conditions + } else { + // Otherwise, expired + this.observability.logging.log('SSO token has expired and will not be auto-refreshed.') + return invalidDelay + } + + this.observability.logging.log(`Auto-refreshing SSO token in ${delayMillis} milliseconds.`) + return delayMillis + } +} diff --git a/server/aws-lsp-identity/src/language-server/identityServer.ts b/server/aws-lsp-identity/src/language-server/identityServer.ts index be5d357ead..f0b194369c 100644 --- a/server/aws-lsp-identity/src/language-server/identityServer.ts +++ b/server/aws-lsp-identity/src/language-server/identityServer.ts @@ -7,17 +7,28 @@ import { AwsErrorCodes, GetSsoTokenParams, InvalidateSsoTokenParams, + InvalidateStsCredentialParams, InitializeParams, PartialInitializeResult, ShowMessageRequestParams, + GetIamCredentialParams, + GetMfaCodeParams, + StsCredentialChangedParams, + SsoTokenChangedParams, + ProfileChangedParams, } from '@aws/language-server-runtimes/server-interface' import { SharedConfigProfileStore } from './profiles/sharedConfigProfileStore' import { IdentityService } from './identityService' import { FileSystemSsoCache, RefreshingSsoCache } from '../sso/cache' -import { SsoTokenAutoRefresher } from './ssoTokenAutoRefresher' +import { SsoTokenAutoRefresher } from '../sso/ssoTokenAutoRefresher' +import { FileSystemStsCache } from '../sts/cache/fileSystemStsCache' +import { StsAutoRefresher } from '../sts/stsAutoRefresher' import { AwsError, ServerBase } from '@aws/lsp-core' import { Features } from '@aws/language-server-runtimes/server-interface/server' -import { ShowUrl, ShowMessageRequest, ShowProgress } from '../sso/utils' +import { ShowUrl, ShowMessageRequest, ShowProgress, SendSsoTokenChanged } from '../sso/utils' +import { SendGetMfaCode, SendProfileChanged, SendStsCredentialChanged } from '../iam/utils' +import { IamProvider } from '../iam/iamProvider' +import { ProfileWatcher } from './profiles/profileWatcher' export class IdentityServer extends ServerBase { constructor(features: Features) { @@ -32,34 +43,50 @@ export class IdentityServer extends ServerBase { protected override async initialize(params: InitializeParams): Promise> { const result = await super.initialize(params) - // Callbacks for server->client JSON-RPC calls + // Callbacks for JSON-RPC calls const showUrl: ShowUrl = (url: URL) => this.features.lsp.window.showDocument({ uri: url.toString(), external: true }) const showMessageRequest: ShowMessageRequest = (params: ShowMessageRequestParams) => this.features.lsp.window.showMessageRequest(params) const showProgress: ShowProgress = this.features.lsp.sendProgress + const sendGetMfaCode: SendGetMfaCode = (params: GetMfaCodeParams) => + this.features.identityManagement.sendGetMfaCode(params) + const sendSsoTokenChanged: SendSsoTokenChanged = (params: SsoTokenChangedParams) => + this.features.identityManagement.sendSsoTokenChanged(params) + const sendStsCredentialChanged: SendStsCredentialChanged = (params: StsCredentialChangedParams) => + this.features.identityManagement.sendStsCredentialChanged(params) + const sendProfileChanged: SendProfileChanged = (params: ProfileChangedParams) => + this.features.identityManagement.sendProfileChanged(params) // Initialize dependencies const profileStore = new SharedConfigProfileStore(this.observability) const ssoCache = new RefreshingSsoCache( new FileSystemSsoCache(this.observability), - this.features.identityManagement.sendSsoTokenChanged, + sendSsoTokenChanged, this.observability ) const autoRefresher = new SsoTokenAutoRefresher(ssoCache, this.observability) + const stsCache = new FileSystemStsCache(this.observability) + const stsAutoRefresher = new StsAutoRefresher(stsCache, sendStsCredentialChanged, this.observability) + const iamProvider = new IamProvider() const identityService = new IdentityService( profileStore, ssoCache, autoRefresher, - { showUrl, showMessageRequest, showProgress }, + stsCache, + stsAutoRefresher, + iamProvider, + { showUrl, showMessageRequest, showProgress, sendGetMfaCode }, this.getClientName(params), this.observability ) const profileService = new ProfileService(profileStore, this.observability) + const profileWatcher = new ProfileWatcher(profileStore, sendProfileChanged, this.observability) + profileWatcher.watch() // JSON-RPC request/notification handlers (client->server) this.features.identityManagement.onGetSsoToken( @@ -70,6 +97,14 @@ export class IdentityServer extends ServerBase { }) ) + this.features.identityManagement.onGetIamCredential( + async (params: GetIamCredentialParams, token: CancellationToken) => + await identityService.getIamCredential(params, token).catch(reason => { + this.observability.logging.log(`GetIamCredential failed. ${reason}`) + throw awsResponseErrorWrap(reason) + }) + ) + this.features.identityManagement.onInvalidateSsoToken( async (params: InvalidateSsoTokenParams, token: CancellationToken) => await identityService.invalidateSsoToken(params, token).catch(reason => { @@ -78,6 +113,14 @@ export class IdentityServer extends ServerBase { }) ) + this.features.identityManagement.onInvalidateStsCredential( + async (params: InvalidateStsCredentialParams, token: CancellationToken) => + await identityService.invalidateStsCredential(params, token).catch(reason => { + this.observability.logging.log(`InvalidateIamCredentials failed. ${reason}`) + throw awsResponseErrorWrap(reason) + }) + ) + this.features.identityManagement.onListProfiles( async (params: ListProfilesParams, token: CancellationToken) => await profileService.listProfiles(params, token).catch(reason => { @@ -95,6 +138,8 @@ export class IdentityServer extends ServerBase { ) this.disposables.push(autoRefresher) + this.disposables.push(stsAutoRefresher) + this.disposables.push(profileWatcher) return { ...result, diff --git a/server/aws-lsp-identity/src/language-server/identityService.test.ts b/server/aws-lsp-identity/src/language-server/identityService.test.ts index 5bf9ff1432..7e5e13f3df 100644 --- a/server/aws-lsp-identity/src/language-server/identityService.test.ts +++ b/server/aws-lsp-identity/src/language-server/identityService.test.ts @@ -3,17 +3,23 @@ import { StubbedInstance, stubInterface } from 'ts-sinon' import { awsBuilderIdReservedName, SsoCache, SsoClientRegistration } from '../sso' import { IdentityService } from './identityService' import { ProfileData, ProfileStore } from './profiles/profileService' -import { SsoTokenAutoRefresher } from './ssoTokenAutoRefresher' -import { createStubInstance, restore, spy, SinonSpy } from 'sinon' +import { SsoTokenAutoRefresher } from '../sso/ssoTokenAutoRefresher' +import { createStubInstance, restore, spy, SinonSpy, stub, SinonStub } from 'sinon' import { AuthorizationFlowKind, CancellationToken, + IamCredential, + IamCredentials, ProfileKind, SsoTokenSourceKind, } from '@aws/language-server-runtimes/protocol' import { SSOToken } from '@smithy/shared-ini-file-loader' import { Logging, Telemetry } from '@aws/language-server-runtimes/server-interface' import { Observability } from '@aws/lsp-core' +import { StsCache } from '../sts/cache/stsCache' +import { StsAutoRefresher } from '../sts/stsAutoRefresher' +import { IamProvider } from '../iam/iamProvider' +import * as iamUtils from '../iam/utils' // eslint-disable-next-line use(require('chai-as-promised')) @@ -22,9 +28,17 @@ let sut: IdentityService let profileStore: StubbedInstance let ssoCache: StubbedInstance +let stsCache: StubbedInstance let autoRefresher: StubbedInstance +let stsAutoRefresher: StubbedInstance +let iamProvider: StubbedInstance let observability: StubbedInstance let authFlowFn: SinonSpy +let credentialProvider: SinonSpy +let validatePermissionsStub: SinonStub< + [credentials: IamCredentials, permissions: string[], region?: string | undefined], + Promise +> describe('IdentityService', () => { beforeEach(() => { @@ -33,11 +47,19 @@ describe('IdentityService', () => { profiles: [ { kinds: [ProfileKind.SsoTokenProfile], - name: 'my-profile', + name: 'my-sso-profile', settings: { sso_session: 'my-sso-session', }, }, + { + kinds: [ProfileKind.IamCredentialsProfile], + name: 'my-iam-profile', + settings: { + aws_access_key_id: 'my-access-key', + aws_secret_access_key: 'my-secret-key', + }, + }, ], ssoSessions: [ { @@ -65,11 +87,33 @@ describe('IdentityService', () => { setSsoToken: Promise.resolve(), }) + stsCache = stubInterface({ + getStsCredential: Promise.resolve(undefined), + setStsCredential: Promise.resolve(), + removeStsCredential: Promise.resolve(), + }) + autoRefresher = createStubInstance(SsoTokenAutoRefresher, { watch: Promise.resolve(), unwatch: undefined, }) as StubbedInstance + stsAutoRefresher = createStubInstance(StsAutoRefresher, { + watch: Promise.resolve(), + unwatch: undefined, + }) as StubbedInstance + + iamProvider = createStubInstance(IamProvider, { + getCredential: Promise.resolve({ + id: 'id', + kinds: [], + credentials: { + accessKeyId: 'access-key', + secretAccessKey: 'secret-key', + }, + } as IamCredential), + }) as StubbedInstance + authFlowFn = spy(() => Promise.resolve({ accessToken: 'my-access-token', @@ -77,6 +121,18 @@ describe('IdentityService', () => { } satisfies SSOToken) ) + credentialProvider = spy(() => { + return () => { + return { + accessKeyId: 'provider-access-key', + secretAccessKey: 'provider-secret-key', + sessionToken: 'provider-session-token', + credentialScope: 'provider-credential-scope', + accountId: 'provider-account-id', + } + } + }) + observability = stubInterface() observability.logging = stubInterface() observability.telemetry = stubInterface() @@ -85,10 +141,14 @@ describe('IdentityService', () => { profileStore, ssoCache, autoRefresher, + stsCache, + stsAutoRefresher, + iamProvider, { showUrl: _ => {}, showMessageRequest: _ => Promise.resolve({ title: 'client-response' }), showProgress: _ => Promise.resolve(), + sendGetMfaCode: () => Promise.resolve({ code: 'mfa-code', mfaSerial: 'mfa-serial' }), }, 'My Client', observability, @@ -97,6 +157,9 @@ describe('IdentityService', () => { [AuthorizationFlowKind.DeviceCode]: authFlowFn, } ) + + validatePermissionsStub = stub(iamUtils, 'validatePermissions') + validatePermissionsStub.resolves(true) }) afterEach(() => { @@ -122,7 +185,7 @@ describe('IdentityService', () => { const actual = await sut.getSsoToken( { clientName: 'my-client', - source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-profile' }, + source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-sso-profile' }, }, CancellationToken.None ) @@ -136,7 +199,7 @@ describe('IdentityService', () => { await sut.getSsoToken( { clientName: 'my-client', - source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-profile' }, + source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-sso-profile' }, options: { authorizationFlow: 'DeviceCode', }, @@ -148,7 +211,7 @@ describe('IdentityService', () => { await sut.getSsoToken( { clientName: 'my-client', - source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-profile' }, + source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-sso-profile' }, options: { authorizationFlow: 'Pkce', }, @@ -185,7 +248,7 @@ describe('IdentityService', () => { const actual = await sut.getSsoToken( { clientName: 'my-client', - source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-profile' }, + source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-sso-profile' }, }, CancellationToken.None ) @@ -203,7 +266,7 @@ describe('IdentityService', () => { const actual = await sut.getSsoToken( { clientName: 'my-client', - source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-profile' }, + source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-sso-profile' }, }, CancellationToken.None ) @@ -255,6 +318,24 @@ describe('IdentityService', () => { }) }) + describe('getIamCredential', () => { + it('Can login with IAM credentials.', async () => { + const actual = await sut.getIamCredential({ profileName: 'my-iam-profile' }, CancellationToken.None) + + expect(actual.credential.credentials.accessKeyId).to.equal('access-key') + expect(actual.credential.credentials.secretAccessKey).to.equal('secret-key') + }) + + it('Throws when permissions are insufficient', async () => { + validatePermissionsStub.resolves(false) + const error = await expect( + sut.getIamCredential({ profileName: 'my-iam-profile' }, CancellationToken.None) + ).rejectedWith(Error) + + expect(error.message).to.equal('Credentials have insufficient permissions.') + }) + }) + describe('invalidateSsoToken', () => { it('removeToken removes on valid SSO session name', async () => { await sut.invalidateSsoToken({ ssoTokenId: 'my-sso-session' }, CancellationToken.None) @@ -268,4 +349,20 @@ describe('IdentityService', () => { expect(ssoCache.removeSsoToken.notCalled).is.true }) }) + + describe('invalidateStsCredential', () => { + it('Removes on valid name', async () => { + await sut.invalidateStsCredential({ iamCredentialId: 'my-role-profile' }, CancellationToken.None) + + expect(stsCache.removeStsCredential.called).is.true + }) + + it('Throws on invalid name', async () => { + await expect( + sut.invalidateStsCredential({ iamCredentialId: ' ' }, CancellationToken.None) + ).to.be.rejectedWith() + + expect(stsCache.removeStsCredential.notCalled).is.true + }) + }) }) diff --git a/server/aws-lsp-identity/src/language-server/identityService.ts b/server/aws-lsp-identity/src/language-server/identityService.ts index 6e0d4a3817..4af2c5d246 100644 --- a/server/aws-lsp-identity/src/language-server/identityService.ts +++ b/server/aws-lsp-identity/src/language-server/identityService.ts @@ -3,12 +3,17 @@ import { AwsBuilderIdSsoTokenSource, AwsErrorCodes, CancellationToken, + GetIamCredentialParams, + GetIamCredentialResult, getSsoTokenOptionsDefaults, + getIamCredentialOptionsDefaults, GetSsoTokenParams, GetSsoTokenResult, IamIdentityCenterSsoTokenSource, InvalidateSsoTokenParams, InvalidateSsoTokenResult, + InvalidateStsCredentialParams, + InvalidateStsCredentialResult, MetricEvent, SsoSession, SsoTokenSourceKind, @@ -16,20 +21,29 @@ import { import { normalizeSettingList, ProfileStore } from './profiles/profileService' import { authorizationCodePkceFlow, awsBuilderIdReservedName, awsBuilderIdSsoRegion } from '../sso' import { SsoCache, SsoClientRegistration } from '../sso/cache' -import { SsoTokenAutoRefresher } from './ssoTokenAutoRefresher' +import { SsoTokenAutoRefresher } from '../sso/ssoTokenAutoRefresher' +import { StsCache } from '../sts/cache/stsCache' +import { StsAutoRefresher } from '../sts/stsAutoRefresher' import { throwOnInvalidClientRegistration, throwOnInvalidSsoSession, throwOnInvalidSsoSessionName, SsoFlowParams, + SsoHandlers, } from '../sso/utils' +import { IamFlowParams, IamHandlers, throwOnInvalidCredentialId, validatePermissions } from '../iam/utils' import { AwsError, Observability } from '@aws/lsp-core' import { __ServiceException } from '@aws-sdk/client-sso-oidc/dist-types/models/SSOOIDCServiceException' import { deviceCodeFlow } from '../sso/deviceCode/deviceCodeFlow' import { SSOToken } from '@smithy/shared-ini-file-loader' +import { fromProcess } from '@aws-sdk/credential-provider-process' +import { fromContainerMetadata, fromInstanceMetadata } from '@smithy/credential-provider-imds' +import { fromEnv } from '@aws-sdk/credential-provider-env' +import { IamProvider } from '../iam/iamProvider' type SsoTokenSource = IamIdentityCenterSsoTokenSource | AwsBuilderIdSsoTokenSource type AuthFlows = Record Promise> +type Handlers = SsoHandlers & IamHandlers const flows: AuthFlows = { [AuthorizationFlowKind.DeviceCode]: deviceCodeFlow, @@ -40,15 +54,18 @@ export class IdentityService { constructor( private readonly profileStore: ProfileStore, private readonly ssoCache: SsoCache, - private readonly autoRefresher: SsoTokenAutoRefresher, - private readonly handlers: SsoFlowParams['handlers'], + private readonly ssoAutoRefresher: SsoTokenAutoRefresher, + private readonly stsCache: StsCache, + private readonly stsAutoRefresher: StsAutoRefresher, + private readonly iamProvider: IamProvider, + private readonly handlers: Handlers, private readonly clientName: string, private readonly observability: Observability, private readonly authFlows: AuthFlows = flows ) {} async getSsoToken(params: GetSsoTokenParams, token: CancellationToken): Promise { - const emitMetric = this.emitMetric.bind(this, 'flareIdentity_getSsoToken', this.getSsoToken.name, Date.now()) + const emitMetric = this.emitSsoMetric.bind(this, 'flareIdentity_getSsoToken', this.getSsoToken.name, Date.now()) let clientRegistration: SsoClientRegistration | undefined let ssoSession: SsoSession | undefined @@ -94,7 +111,7 @@ export class IdentityService { clientName: this.clientName, clientRegistration, ssoSession, - handlers: this.handlers, + handlers: this.handlers as SsoHandlers, token, observability: this.observability, } @@ -120,7 +137,7 @@ export class IdentityService { } // Auto refresh is best effort - await this.autoRefresher.watch(this.clientName, ssoSession).catch(reason => { + await this.ssoAutoRefresher.watch(this.clientName, ssoSession).catch(reason => { this.observability.logging.log(`Unable to auto-refresh token. ${reason}`) }) @@ -136,11 +153,76 @@ export class IdentityService { } } + async getIamCredential(params: GetIamCredentialParams, token: CancellationToken): Promise { + const emitMetric = this.emitIamMetric.bind( + this, + 'flareIdentity_getIamCredential', + this.getIamCredential.name, + Date.now() + ) + + try { + const options = { ...getIamCredentialOptionsDefaults, ...params.options } + + token.onCancellationRequested(_ => { + emitMetric('Cancelled', null) + }) + + // Get the profile with provided name + const profileData = await this.profileStore.load() + const profile = profileData.profiles.find(p => p.name === params.profileName) + if (!profile) { + this.observability.logging.log('Profile not found.') + throw new AwsError('Profile not found.', AwsErrorCodes.E_PROFILE_NOT_FOUND) + } + + const flowOpts: IamFlowParams = { + profile: profile, + callStsOnInvalidIamCredential: options.callStsOnInvalidIamCredential, + recursionCount: 0, + profileStore: this.profileStore, + stsCache: this.stsCache, + stsAutoRefresher: this.stsAutoRefresher, + handlers: this.handlers as IamHandlers, + providers: { + fromProcess: fromProcess, + fromContainerMetadata: fromContainerMetadata, + fromInstanceMetadata: fromInstanceMetadata, + fromEnv: fromEnv, + }, + token: token, + emitMetric: emitMetric, + observability: this.observability, + } + const credential = await this.iamProvider.getCredential(flowOpts) + + // Validate permissions + if (options.permissionSet.length > 0) { + const hasPermissions = await validatePermissions( + credential.credentials, + options.permissionSet, + profile.settings?.region + ) + if (!hasPermissions) { + throw new AwsError(`Credentials have insufficient permissions.`, AwsErrorCodes.E_INVALID_PROFILE) + } + } + + return { + credential: credential, + updateCredentialsParams: { data: credential.credentials, encrypted: false }, + } + } catch (e) { + emitMetric('Failed', e) + throw e + } + } + async invalidateSsoToken( params: InvalidateSsoTokenParams, token: CancellationToken ): Promise { - const emitMetric = this.emitMetric.bind( + const emitMetric = this.emitSsoMetric.bind( this, 'flareIdentity_invalidateSsoToken', this.invalidateSsoToken.name, @@ -154,7 +236,7 @@ export class IdentityService { try { throwOnInvalidSsoSessionName(params?.ssoTokenId) - this.autoRefresher.unwatch(params.ssoTokenId) + this.ssoAutoRefresher.unwatch(params.ssoTokenId) await this.ssoCache.removeSsoToken(params.ssoTokenId) @@ -168,7 +250,38 @@ export class IdentityService { } } - private emitMetric( + async invalidateStsCredential( + params: InvalidateStsCredentialParams, + token: CancellationToken + ): Promise { + const emitMetric = this.emitIamMetric.bind( + this, + 'flareIdentity_invalidateStsCredential', + this.invalidateStsCredential.name, + Date.now() + ) + + token.onCancellationRequested(_ => { + emitMetric('Cancelled') + }) + + try { + throwOnInvalidCredentialId(params.iamCredentialId) + + this.stsAutoRefresher.unwatch(params.iamCredentialId) + + await this.stsCache.removeStsCredential(params.iamCredentialId) + + emitMetric('Succeeded') + this.observability.logging.log('Successfully invalidated STS credentials.') + return {} + } catch (e) { + emitMetric('Failed', e) + throw e + } + } + + private emitSsoMetric( name: string, source: string, startMillis: number, @@ -205,6 +318,37 @@ export class IdentityService { this.observability.telemetry.emitMetric(metric) } + private emitIamMetric( + name: string, + source: string, + startMillis: number, + result: 'Succeeded' | 'Failed' | 'Cancelled', + error?: unknown, + credentialType?: string + ): void { + const metric: MetricEvent = { + name, + result, + data: { + duration: Date.now() - startMillis, + source, + credentialType, + }, + } + + if (error) { + metric.errorData = { + errorCode: (error as AwsError)?.awsErrorCode, + httpStatusCode: + (error as __ServiceException)?.$metadata?.httpStatusCode || + ((error as Error).cause as __ServiceException)?.$metadata?.httpStatusCode, + reason: error?.constructor?.name ?? 'unknown', + } + } + + this.observability.telemetry.emitMetric(metric) + } + private async getSsoSession(source: SsoTokenSource): Promise { switch (source.kind) { case SsoTokenSourceKind.AwsBuilderId: diff --git a/server/aws-lsp-identity/src/language-server/profiles/profileService.test.ts b/server/aws-lsp-identity/src/language-server/profiles/profileService.test.ts index f2da8c62e9..216f3010af 100644 --- a/server/aws-lsp-identity/src/language-server/profiles/profileService.test.ts +++ b/server/aws-lsp-identity/src/language-server/profiles/profileService.test.ts @@ -23,6 +23,7 @@ let observability: StubbedInstance let profile1: Profile let profile2: Profile let profile3: Profile +let profile4: Profile let ssoSession1: SsoSession let ssoSession2: SsoSession @@ -52,6 +53,16 @@ describe('ProfileService', async () => { }, } + profile4 = { + kinds: [ProfileKind.IamCredentialsProfile], + name: 'profile4', + settings: { + aws_access_key_id: 'access-key', + aws_secret_access_key: 'secret-key', + aws_session_token: 'session-token', + }, + } + ssoSession1 = { name: 'ssoSession1', settings: { @@ -71,7 +82,7 @@ describe('ProfileService', async () => { store = stubInterface({ load: Promise.resolve({ - profiles: [profile1, profile2, profile3], + profiles: [profile1, profile2, profile3, profile4], ssoSessions: [ssoSession1, ssoSession2], } satisfies ProfileData), save: Promise.resolve(), @@ -87,7 +98,7 @@ describe('ProfileService', async () => { it('listProfiles return profiles and sso-sessions', async () => { const actual = await sut.listProfiles({}) - expect(actual.profiles).to.be.an('array').that.has.deep.members([profile1, profile2, profile3]) + expect(actual.profiles).to.be.an('array').that.has.deep.members([profile1, profile2, profile3, profile4]) expect(actual.ssoSessions).to.be.an('array').that.has.deep.members([ssoSession1, ssoSession2]) }) @@ -198,23 +209,6 @@ describe('ProfileService', async () => { expectAwsError(sut, { profile: undefined! }, AwsErrorCodes.E_INVALID_PROFILE, 'Profile required.') }) - it('updateProfile throws on non-SSO token profile', async () => { - const profile = { - kinds: [ProfileKind.Unknown], - name: 'profile-name', - settings: { - sso_session: 'sso-session-name', - }, - } - - await expectAwsError( - sut, - { profile }, - AwsErrorCodes.E_INVALID_PROFILE, - 'Profile must be non-legacy sso-session type.' - ) - }) - it('updateProfile throws on no profile name', async () => { const profile = { kinds: [ProfileKind.SsoTokenProfile], @@ -253,7 +247,7 @@ describe('ProfileService', async () => { await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Sso-session name required on profile.') }) - it('updateProfile throws on no sso-session on profile', async () => { + it('updateProfile throws on no sso-session on SSO token profile', async () => { const profile = { kinds: [ProfileKind.SsoTokenProfile], name: 'profile-name', @@ -265,6 +259,100 @@ describe('ProfileService', async () => { await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Sso-session name required on profile.') }) + it('updateProfile throws on missing access key for IamCredentialsProfile', async () => { + const profile = { + kinds: [ProfileKind.IamCredentialsProfile], + name: 'profile-name', + settings: { + aws_secret_access_key: 'secret-key', + }, + } + + await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Access key required on profile.') + }) + + it('updateProfile throws on missing secret key for IamCredentialsProfile', async () => { + const profile = { + kinds: [ProfileKind.IamCredentialsProfile], + name: 'profile-name', + settings: { + aws_access_key_id: 'access-key', + }, + } + + await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Secret key required on profile.') + }) + + it('updateProfile throws on missing role ARN for IamSourceProfileProfile', async () => { + const profile = { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'profile-name', + settings: { + source_profile: 'source', + }, + } + + await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Role ARN required on profile.') + }) + + it('updateProfile throws on missing source profile for IamSourceProfileProfile', async () => { + const profile = { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'profile-name', + settings: { + role_arn: 'role-arn', + }, + } + + await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Source profile required on profile.') + }) + + it('updateProfile throws on missing role ARN for IamCredentialSourceProfile', async () => { + const profile = { + kinds: [ProfileKind.IamCredentialSourceProfile], + name: 'profile-name', + settings: { + credential_source: 'Ec2InstanceMetadata', + region: 'region', + }, + } + + await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Role ARN required on profile.') + }) + + it('updateProfile throws on missing credential source for IamCredentialSourceProfile', async () => { + const profile = { + kinds: [ProfileKind.IamCredentialSourceProfile], + name: 'profile-name', + settings: { + role_arn: 'role-arn', + region: 'region', + }, + } + + await expectAwsError( + sut, + { profile }, + AwsErrorCodes.E_INVALID_PROFILE, + 'Credential source required on profile.' + ) + }) + + it('updateProfile throws on missing credential process for process profile', async () => { + const profile = { + kinds: [ProfileKind.IamCredentialProcessProfile], + name: 'profile-name', + settings: {}, + } + + await expectAwsError( + sut, + { profile }, + AwsErrorCodes.E_INVALID_PROFILE, + 'Credential process required on profile.' + ) + }) + it('updateProfile throws when profile cannot be created', async () => { const profile = { kinds: [ProfileKind.SsoTokenProfile], @@ -411,7 +499,7 @@ describe('ProfileService', async () => { }) describe('profileService.DuckTypers', () => { - it('profileDuckTypers.eval returns true on valid profiles', () => { + it('profileDuckTypers.SsoTokenProfile.eval returns true on valid profiles', () => { const profiles = [ { sso_session: 'my-sso-session', @@ -428,7 +516,7 @@ describe('profileService.DuckTypers', () => { } }) - it('profileDuckTypers returns false on invalid profiles', () => { + it('profileDuckTypers.SsoTokenProfile.eval returns false on invalid profiles', () => { const profiles = [ { SSO_session: 'my-sso-session', @@ -484,6 +572,102 @@ describe('profileService.DuckTypers', () => { expect(actual).to.be.false } }) + + it('profileDuckTypers.IamCredentialsProfile.eval returns true on valid profiles', () => { + const profiles = [ + { + aws_access_key_id: 'access-key', + aws_secret_access_key: 'secret-key', + }, + { + aws_access_key_id: 'access-key', + aws_secret_access_key: 'secret-key', + aws_session_token: 'session-token', + }, + ] + + for (const profile of profiles) { + const actual = profileDuckTypers.IamCredentialsProfile.eval(profile) + expect(actual).to.be.true + } + }) + + it('profileDuckTypers.IamCredentialsProfile.eval returns false on invalid profiles', () => { + const profiles = [ + { + sso_session: 'my-sso-session', + }, + null, + { + sso_account_id: '123', + }, + ] + + for (const profile of profiles) { + const actual = profileDuckTypers.IamCredentialsProfile.eval(profile as object) + expect(actual).to.be.false + } + }) + + it('profileDuckTypers.IamSourceProfileProfile.eval returns true on valid profiles', () => { + const profiles = [ + { + role_arn: 'role-arn', + source_profile: 'source-profile', + }, + { + role_arn: 'role-arn', + source_profile: 'source-profile', + role_session_name: 'role-session-name', + mfa_serial: 'mfa-serial', + }, + ] + + for (const profile of profiles) { + const actual = profileDuckTypers.IamSourceProfileProfile.eval(profile) + expect(actual).to.be.true + } + }) + + it('profileDuckTypers.IamCredentialSourceProfile.eval returns true on valid profiles', () => { + const profiles = [ + { + role_arn: 'role-arn', + credential_source: 'credential-source', + region: 'region', + }, + { + role_arn: 'role-arn', + credential_source: 'credential-source', + region: 'region', + role_session_name: 'role-session-name', + }, + ] + + for (const profile of profiles) { + const actual = profileDuckTypers.IamCredentialSourceProfile.eval(profile) + expect(actual).to.be.true + } + }) + + it('profileDuckTypers.IamCredentialProcessProfile.eval returns true on valid profiles', () => { + const profiles = [ + { + credential_process: 'credential-process', + }, + { + aws_access_key_id: 'access-key', + aws_secret_access_key: 'secret-key', + aws_session_token: 'session-token', + credential_process: 'credential-process', + }, + ] + + for (const profile of profiles) { + const actual = profileDuckTypers.IamCredentialProcessProfile.eval(profile) + expect(actual).to.be.true + } + }) }) describe('profileService.functions', () => { diff --git a/server/aws-lsp-identity/src/language-server/profiles/profileService.ts b/server/aws-lsp-identity/src/language-server/profiles/profileService.ts index 247ccd1833..a37e27e30e 100644 --- a/server/aws-lsp-identity/src/language-server/profiles/profileService.ts +++ b/server/aws-lsp-identity/src/language-server/profiles/profileService.ts @@ -30,6 +30,18 @@ export const ProfileFields = { sso_account_id: 'sso_account_id', sso_role_name: 'sso_role_name', sso_session: 'sso_session', + aws_access_key_id: 'aws_access_key_id', + aws_secret_access_key: 'aws_secret_access_key', + aws_session_token: 'aws_session_token', + role_arn: 'role_arn', + role_session_name: 'role_session_name', + credential_process: 'credential_process', + credential_source: 'credential_source', + source_profile: 'source_profile', + mfa_serial: 'mfa_serial', + external_id: 'external_id', + credential_cache: 'credential_cache', + credential_cache_location: 'credential_cache_location', } as const export const SsoSessionFields = { @@ -38,12 +50,70 @@ export const SsoSessionFields = { sso_start_url: 'sso_start_url', } as const -export const profileDuckTypers = { - SsoTokenProfile: new DuckTyper() - .requireProperty(ProfileFields.sso_session) - .disallowProperty(ProfileFields.sso_account_id) - .disallowProperty(ProfileFields.sso_role_name), -} +export const profileTypes = { + SsoTokenProfile: { + kind: ProfileKind.SsoTokenProfile, + required: [ProfileFields.sso_session], + optional: [ProfileFields.region], + disallowed: [ProfileFields.sso_account_id, ProfileFields.sso_role_name], + }, + IamCredentialsProfile: { + kind: ProfileKind.IamCredentialsProfile, + required: [ProfileFields.aws_access_key_id, ProfileFields.aws_secret_access_key], + optional: [ProfileFields.aws_session_token], + disallowed: [], + }, + IamSourceProfileProfile: { + kind: ProfileKind.IamSourceProfileProfile, + required: [ProfileFields.role_arn, ProfileFields.source_profile], + optional: [ + ProfileFields.external_id, + ProfileFields.role_session_name, + ProfileFields.region, + ProfileFields.mfa_serial, + ProfileFields.credential_cache, + ProfileFields.credential_cache_location, + ], + disallowed: [ProfileFields.credential_source], + }, + IamCredentialSourceProfile: { + kind: ProfileKind.IamCredentialSourceProfile, + required: [ProfileFields.role_arn, ProfileFields.credential_source], + optional: [ + ProfileFields.external_id, + ProfileFields.role_session_name, + ProfileFields.region, + ProfileFields.credential_cache, + ProfileFields.credential_cache_location, + ], + disallowed: [ProfileFields.source_profile], + }, + IamCredentialProcessProfile: { + kind: ProfileKind.IamCredentialProcessProfile, + required: [ProfileFields.credential_process], + optional: [], + disallowed: [], + }, +} as const + +export const profileDuckTypers = Object.fromEntries( + Object.entries(profileTypes).map(([key, def]) => [ + key, + (() => { + const typer = new DuckTyper() + for (const field of def.required) { + typer.requireProperty(field) + } + for (const field of def.optional) { + typer.optionalProperty(field) + } + for (const field of def.disallowed) { + typer.disallowProperty(field) + } + return typer + })(), + ]) +) export const ssoSessionDuckTyper = new DuckTyper() .requireProperty(SsoSessionFields.sso_start_url) @@ -94,53 +164,78 @@ export class ProfileService { const profile = params.profile! this.throwOnInvalidProfile( - !profile.kinds.includes(ProfileKind.SsoTokenProfile), - 'Profile must be non-legacy sso-session type.' + !profile.kinds.some(kind => Object.values(ProfileKind).includes(kind)), + 'Profile must be non-legacy sso-session or iam-credentials type.' ) this.throwOnInvalidProfile(!profile.name, 'Profile name required.') this.throwOnInvalidProfile(!profile.settings, 'Settings required on profile.') const profileSettings = profile.settings! - this.throwOnInvalidProfile(!profileSettings.sso_session, 'Sso-session name required on profile.') - - // Validate sso-session - this.throwOnInvalidSsoSession(!params.ssoSession, 'Sso-session required.') - const ssoSession: SsoSession = params.ssoSession! - - this.throwOnInvalidSsoSession(!ssoSession.name, 'Sso-session name required.') - this.throwOnInvalidSsoSession(!ssoSession.settings, 'Settings required on sso-session.') - const ssoSessionSettings = ssoSession.settings! - - this.throwOnInvalidSsoSession(!ssoSessionSettings.sso_region, 'Sso-session region required.') - this.throwOnInvalidSsoSession(!ssoSessionSettings.sso_start_url, 'Sso-session start URL required.') - - this.throwOnInvalidProfile( - profileSettings.sso_session !== ssoSession.name, - 'Profile sso-session name must be the same as provided sso-session.' - ) - + // Get profiles and SSO sessions to check whether a duplicate will be created const { profiles, ssoSessions } = await this.profileStore.load().catch(reason => { throw AwsError.wrap(reason, AwsErrorCodes.E_CANNOT_READ_SHARED_CONFIG) }) - // Enforce options + // Check if the profile can be created if (!options.createNonexistentProfile && !profiles.some(p => p.name === profile.name)) { this.observability.logging.log(`Cannot create profile. options: ${JSON.stringify(options)}`) throw new AwsError('Cannot create profile.', AwsErrorCodes.E_CANNOT_CREATE_PROFILE) } - if (!options.createNonexistentSsoSession && !ssoSessions.some(s => s.name === ssoSession.name)) { - this.observability.logging.log(`Cannot create sso-session. options: ${JSON.stringify(options)}`) - throw new AwsError('Cannot create sso-session.', AwsErrorCodes.E_CANNOT_CREATE_SSO_SESSION) + // TODO: can this be refactored and simplified using the existing DuckTypers? + // Validate SSO profile + if (profile.kinds.includes(ProfileKind.SsoTokenProfile)) { + this.throwOnInvalidProfile(!profileSettings.sso_session, 'Sso-session name required on profile.') + this.throwOnInvalidSsoSession(!params.ssoSession, 'Sso-session required.') + const ssoSession: SsoSession = params.ssoSession! + + this.throwOnInvalidSsoSession(!ssoSession.name, 'Sso-session name required.') + this.throwOnInvalidSsoSession(!ssoSession.settings, 'Settings required on sso-session.') + const ssoSessionSettings = ssoSession.settings! + + this.throwOnInvalidSsoSession(!ssoSessionSettings.sso_region, 'Sso-session region required.') + this.throwOnInvalidSsoSession(!ssoSessionSettings.sso_start_url, 'Sso-session start URL required.') + + this.throwOnInvalidProfile( + profileSettings.sso_session !== ssoSession.name, + 'Profile sso-session name must be the same as provided sso-session.' + ) + + // Check if the SSO session can be created + if (!options.createNonexistentSsoSession && !ssoSessions.some(s => s.name === ssoSession.name)) { + this.observability.logging.log(`Cannot create sso-session. options: ${JSON.stringify(options)}`) + throw new AwsError('Cannot create sso-session.', AwsErrorCodes.E_CANNOT_CREATE_SSO_SESSION) + } + + // Check if the SSO session can be updated + if ( + !options.updateSharedSsoSession && + this.isSharedSsoSession(ssoSession.name, profiles, profile.name) && + this.willUpdateExistingSsoSession(ssoSession, ssoSessions) + ) { + this.observability.logging.log(`Cannot update shared sso-session. options: ${JSON.stringify(options)}`) + throw new AwsError('Cannot update shared sso-session.', AwsErrorCodes.E_CANNOT_OVERWRITE_SSO_SESSION) + } + } + + // Validate IAM profiles + if (profile.kinds.includes(ProfileKind.IamCredentialsProfile)) { + this.throwOnInvalidProfile(!profileSettings.aws_access_key_id, 'Access key required on profile.') + this.throwOnInvalidProfile(!profileSettings.aws_secret_access_key, 'Secret key required on profile.') + } + + if (profile.kinds.includes(ProfileKind.IamCredentialSourceProfile)) { + this.throwOnInvalidProfile(!profileSettings.role_arn, 'Role ARN required on profile.') + this.throwOnInvalidProfile(!profileSettings.credential_source, 'Credential source required on profile.') + } + + if (profile.kinds.includes(ProfileKind.IamSourceProfileProfile)) { + this.throwOnInvalidProfile(!profileSettings.role_arn, 'Role ARN required on profile.') + this.throwOnInvalidProfile(!profileSettings.source_profile, 'Source profile required on profile.') } - if ( - !options.updateSharedSsoSession && - this.isSharedSsoSession(ssoSession.name, profiles, profile.name) && - this.willUpdateExistingSsoSession(ssoSession, ssoSessions) - ) { - this.observability.logging.log(`Cannot update shared sso-session. options: ${JSON.stringify(options)}`) - throw new AwsError('Cannot update shared sso-session.', AwsErrorCodes.E_CANNOT_OVERWRITE_SSO_SESSION) + if (profile.kinds.includes(ProfileKind.IamCredentialProcessProfile)) { + this.throwOnInvalidProfile(!profileSettings.credential_process, 'Credential process required on profile.') } await this.profileStore diff --git a/server/aws-lsp-identity/src/language-server/profiles/profileWatcher.test.ts b/server/aws-lsp-identity/src/language-server/profiles/profileWatcher.test.ts new file mode 100644 index 0000000000..58a05c764e --- /dev/null +++ b/server/aws-lsp-identity/src/language-server/profiles/profileWatcher.test.ts @@ -0,0 +1,78 @@ +// eslint-disable-next-line @typescript-eslint/no-require-imports +import mock = require('mock-fs') +import { ProfileData, ProfileStore } from './profileService' +import { Logging, Telemetry } from '@aws/language-server-runtimes/server-interface' +import { SinonSpy, spy, SinonFakeTimers, useFakeTimers } from 'sinon' +import { StubbedInstance, stubInterface } from 'ts-sinon' +import { expect, use } from 'chai' +import { Observability } from '@aws/lsp-core' +import { ProfileWatcher } from './profileWatcher' + +// eslint-disable-next-line @typescript-eslint/no-require-imports +use(require('chai-as-promised')) + +let sut: ProfileWatcher + +let store: StubbedInstance +let sendProfileChangedSpy: SinonSpy +let observability: StubbedInstance +let clock: SinonFakeTimers + +describe('ProfileWatcher', async () => { + beforeEach(() => { + store = stubInterface({ + load: Promise.resolve({ + profiles: [], + ssoSessions: [], + } satisfies ProfileData), + save: Promise.resolve(), + }) + + sendProfileChangedSpy = spy() + + observability = stubInterface() + observability.logging = stubInterface() + observability.telemetry = stubInterface() + + clock = useFakeTimers() + + sut = new ProfileWatcher(store, sendProfileChangedSpy, observability) + }) + + afterEach(() => { + mock.restore() + clock.restore() + }) + + it('should send notification when file is changed', async () => { + // mock-fs does not support fs.watch, so we are not testing watch() directly + // https://github.com/tschaub/mock-fs/issues/246 + sut.onFileChange() + // Wait for debounce timer and its async callback to finish + clock.runAll() + await Promise.resolve() + expect(sendProfileChangedSpy.calledOnce).to.be.true + }) + + it('should only send 1 notification after multiple file changes over short duration', async () => { + sut.onFileChange() + clock.tick(100) + sut.onFileChange() + clock.tick(100) + sut.onFileChange() + + clock.runAll() + await Promise.resolve() + expect(sendProfileChangedSpy.calledOnce).to.be.true + }) + + it('should watch without errors when called multiple times', () => { + sut.watch() + sut.watch() + }) + + it('should unwatch without errors when called multiple times', () => { + sut.unwatch() + sut.unwatch() + }) +}) diff --git a/server/aws-lsp-identity/src/language-server/profiles/profileWatcher.ts b/server/aws-lsp-identity/src/language-server/profiles/profileWatcher.ts new file mode 100644 index 0000000000..88ebda06bb --- /dev/null +++ b/server/aws-lsp-identity/src/language-server/profiles/profileWatcher.ts @@ -0,0 +1,72 @@ +import { FSWatcher, watch } from 'fs' +import { SendProfileChanged } from '../../iam/utils' +import { ProfileStore } from './profileService' +import { Observability } from '@aws/lsp-core' +import { getHomeDir } from '@smithy/shared-ini-file-loader' +import { join } from 'path' + +// Minimum period between file changes to send updated profile notifications +export const fileDebounceMillis = 500 + +export class ProfileWatcher implements Disposable { + private fileWatchers: FSWatcher[] = [] + private debounceTimeout?: NodeJS.Timeout + + constructor( + private profileStore: ProfileStore, + private readonly raiseProfileChanged: SendProfileChanged, + private readonly observability: Observability + ) {} + + [Symbol.dispose](): void { + this.unwatch() + } + + watch(): void { + if (this.fileWatchers.length > 0) { + return + } + + const filepaths = [this.getConfigFilepath(), this.getCredentialsFilepath()] + for (const filepath of filepaths) { + this.fileWatchers.push(watch(filepath, { persistent: false }, this.onFileChange.bind(this))) + } + } + + unwatch(): void { + this.fileWatchers.forEach(watcher => watcher.close()) + this.fileWatchers = [] + } + + onFileChange() { + // Reset the debounce time if this change occurred shortly after previous change + if (this.debounceTimeout) { + clearTimeout(this.debounceTimeout) + } + // Send profile change notification after debounce time elapses + this.debounceTimeout = setTimeout(async () => { + try { + const response = await this.profileStore.load() + this.raiseProfileChanged(response) + } catch (error) { + this.observability.logging.log(`Error reloading profiles: ${error}`) + } + }, fileDebounceMillis) + } + + private getConfigFilepath(): string { + const envVar = process.env['AWS_CONFIG_FILE'] + if (envVar) { + return envVar.startsWith('~/') ? join(getHomeDir(), envVar.substring(2)) : envVar + } + return join(getHomeDir(), '.aws', 'config') + } + + private getCredentialsFilepath(): string { + const envVar = process.env['AWS_SHARED_CREDENTIALS_FILE'] + if (envVar) { + return envVar.startsWith('~/') ? join(getHomeDir(), envVar.substring(2)) : envVar + } + return join(getHomeDir(), '.aws', 'credentials') + } +} diff --git a/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.test.ts b/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.test.ts index c6d938e53c..fa0e85bb72 100644 --- a/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.test.ts +++ b/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.test.ts @@ -81,7 +81,7 @@ describe('SharedConfigProfileStore', async () => { mock.restore() }) - it('loads SSO token profiles and sso-sessions, but not services', async () => { + it('loads profiles and sso-sessions', async () => { setupTest(config, credentials) const actual = await sut.load() @@ -89,20 +89,17 @@ describe('SharedConfigProfileStore', async () => { expect(actual).to.deep.equal({ profiles: [ { - kinds: [ProfileKind.Unknown], + kinds: [ProfileKind.IamCredentialsProfile], name: 'default', settings: { - region: 'us-west-2', - sso_session: undefined, + aws_access_key_id: 'AAAAAAAA', + aws_secret_access_key: 'BBBBBBBB', }, }, { kinds: [ProfileKind.Unknown], name: 'subsettings', - settings: { - region: undefined, - sso_session: undefined, - }, + settings: {}, }, { kinds: [ProfileKind.SsoTokenProfile], @@ -183,26 +180,22 @@ describe('SharedConfigProfileStore', async () => { expect(after).to.deep.equal({ profiles: [ { - kinds: ['Unknown'], + kinds: [ProfileKind.IamCredentialsProfile], name: 'default', settings: { - region: 'us-west-2', - sso_session: undefined, + aws_access_key_id: 'AAAAAAAA', + aws_secret_access_key: 'BBBBBBBB', }, }, { - kinds: ['Unknown'], + kinds: [ProfileKind.Unknown], name: 'subsettings', - settings: { - region: undefined, - sso_session: undefined, - }, + settings: {}, }, { kinds: [ProfileKind.SsoTokenProfile], name: 'config-only.profile', settings: { - region: undefined, sso_session: 'test-sso-session', }, }, @@ -283,20 +276,17 @@ describe('SharedConfigProfileStore', async () => { expect(after).to.deep.equal({ profiles: [ { - kinds: ['Unknown'], + kinds: [ProfileKind.IamCredentialsProfile], name: 'default', settings: { - region: 'us-west-2', - sso_session: undefined, + aws_access_key_id: 'AAAAAAAA', + aws_secret_access_key: 'BBBBBBBB', }, }, { - kinds: ['Unknown'], + kinds: [ProfileKind.Unknown], name: 'subsettings', - settings: { - region: undefined, - sso_session: undefined, - }, + settings: {}, }, { kinds: ['SsoTokenProfile'], @@ -325,6 +315,38 @@ describe('SharedConfigProfileStore', async () => { sso_session: 'new-sso-session', }, }, + { + kinds: [ProfileKind.IamCredentialsProfile], + name: 'iam-user.profile', + settings: { + aws_access_key_id: 'new-access-key', + aws_secret_access_key: 'new-secret-key', + aws_session_token: 'new-session-token', + }, + }, + { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'role-source.profile', + settings: { + role_arn: 'new-role-arn', + source_profile: 'new-source-profile', + }, + }, + { + kinds: [ProfileKind.IamCredentialSourceProfile], + name: 'role-instance.profile', + settings: { + role_arn: 'new-role-arn', + credential_source: 'new-source', + }, + }, + { + kinds: [ProfileKind.IamCredentialProcessProfile], + name: 'process.profile', + settings: { + credential_process: 'new-credential-process', + }, + }, ], ssoSessions: [ { @@ -348,51 +370,74 @@ describe('SharedConfigProfileStore', async () => { await sut.save(data) const after = await sut.load() + after.profiles.sort((a, b) => a.name.localeCompare(b.name)) + after.ssoSessions.sort((a, b) => a.name.localeCompare(b.name)) expect(after).to.deep.equal({ profiles: [ { - kinds: ['Unknown'], + kinds: [ProfileKind.SsoTokenProfile], + name: 'config-only.profile', + settings: { + region: 'us-west-1', + sso_session: 'new-sso-session', + }, + }, + { + kinds: [ProfileKind.SsoTokenProfile], + name: 'credentials-only.profile', + settings: { + region: 'us-east-1', + sso_session: 'test-sso-session', + }, + }, + { + kinds: [ProfileKind.IamCredentialsProfile], name: 'default', settings: { - region: 'us-west-2', - sso_session: undefined, + aws_access_key_id: 'AAAAAAAA', + aws_secret_access_key: 'BBBBBBBB', }, }, { - kinds: ['Unknown'], - name: 'subsettings', + kinds: [ProfileKind.IamCredentialsProfile], + name: 'iam-user.profile', settings: { - region: undefined, - sso_session: undefined, + aws_access_key_id: 'new-access-key', + aws_secret_access_key: 'new-secret-key', + aws_session_token: 'new-session-token', }, }, { - kinds: [ProfileKind.SsoTokenProfile], - name: 'config-only.profile', + kinds: [ProfileKind.IamCredentialProcessProfile], + name: 'process.profile', settings: { - region: 'us-west-1', - sso_session: 'new-sso-session', + credential_process: 'new-credential-process', }, }, { - kinds: [ProfileKind.SsoTokenProfile], - name: 'credentials-only.profile', + kinds: [ProfileKind.IamCredentialSourceProfile], + name: 'role-instance.profile', settings: { - region: 'us-east-1', - sso_session: 'test-sso-session', + role_arn: 'new-role-arn', + credential_source: 'new-source', }, }, - ], - ssoSessions: [ { - name: 'test-sso-session', + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'role-source.profile', settings: { - sso_region: 'us-east-1', - sso_registration_scopes: ['my-scope'], - sso_start_url: 'http://newnowhere', + role_arn: 'new-role-arn', + source_profile: 'new-source-profile', }, }, + { + kinds: [ProfileKind.Unknown], + name: 'subsettings', + settings: {}, + }, + ], + ssoSessions: [ { name: 'new-sso-session', settings: { @@ -400,6 +445,14 @@ describe('SharedConfigProfileStore', async () => { sso_start_url: 'http://somewhere', }, }, + { + name: 'test-sso-session', + settings: { + sso_region: 'us-east-1', + sso_registration_scopes: ['my-scope'], + sso_start_url: 'http://newnowhere', + }, + }, ], }) }) diff --git a/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.ts b/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.ts index 2edbe2bb0f..0b612a46a0 100644 --- a/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.ts +++ b/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.ts @@ -3,11 +3,12 @@ import { ProfileData, profileDuckTypers, ProfileStore, + profileTypes, ssoSessionDuckTyper, } from './profileService' import { parseKnownFiles, SharedConfigInit } from '@smithy/shared-ini-file-loader' import { IniSection, IniSectionType, ParsedIniData } from '@smithy/types' -import { AwsErrorCodes, ProfileKind, SsoSession } from '@aws/language-server-runtimes/server-interface' +import { AwsErrorCodes, Profile, ProfileKind, SsoSession } from '@aws/language-server-runtimes/server-interface' import { SectionHeader } from '../../sharedConfig/types' import { saveKnownFiles } from '../../sharedConfig' import { normalizeParsedIniData } from '../../sharedConfig/saveKnownFiles' @@ -44,22 +45,33 @@ export class SharedConfigProfileStore implements ProfileStore { for (const [parsedSectionName, settings] of Object.entries(parsedIni)) { const sectionHeader = SectionHeader.fromParsedSectionName(parsedSectionName) switch (sectionHeader.type) { - case IniSectionType.PROFILE: - result.profiles.push({ - kinds: [ - // As more profile kinds are added this will get more complex and need refactored - profileDuckTypers.SsoTokenProfile.eval(settings) - ? ProfileKind.SsoTokenProfile - : ProfileKind.Unknown, - ], + // Convert config file profile into profile object + case IniSectionType.PROFILE: { + const profile: Profile = { + kinds: [], name: sectionHeader.name, - settings: { - // Only apply settings expected on Profile - region: settings.region, - sso_session: settings.sso_session, - }, - }) + settings: {}, + } + // Add the kinds and settings for each matched profile type + for (const [profileType, fields] of Object.entries(profileTypes)) { + if (profileDuckTypers[profileType].eval(settings)) { + profile.kinds.push(fields.kind) + const relevantFields = [...fields.required, ...fields.optional] + for (const field of relevantFields) { + if (settings[field] !== undefined) { + profile.settings![field] = settings[field] + } + } + } + } + // If the profile does not match any profile type, mark it as an unknown profile + if (profile.kinds.length === 0) { + profile.kinds.push(ProfileKind.Unknown) + } + result.profiles.push(profile) break + } + // Convert config file SSO session into SSO session object case IniSectionType.SSO_SESSION: { if (!ssoSessionDuckTyper.eval(settings)) { continue @@ -91,9 +103,9 @@ export class SharedConfigProfileStore implements ProfileStore { return result } - // If a setting is set to undefined or null, it will be removed from shared config files - // If the settings property is set to undefined or null, the entire section will be removed - // from the shared config files. This is equivalent to deleting a section. + // If a setting is set to undefined, null, or an empty string, it will be removed from shared + // config files. If the settings property is set to undefined or null, the entire section will + // be removed from the shared config files. This is equivalent to deleting a section. // Any settings or sections in the shared config files that are not passed into data will // be preserved as-is. async save(data: ProfileData, init?: SharedConfigInit): Promise { @@ -114,9 +126,12 @@ export class SharedConfigProfileStore implements ProfileStore { IniSectionType.PROFILE, data.profiles, parsedKnownFiles, - (section, parsedSection) => - !section.kinds.includes(ProfileKind.SsoTokenProfile) || - profileDuckTypers.SsoTokenProfile.eval(parsedSection) + (section, parsedSection) => { + return section.kinds.every(kind => { + const duckTyper = profileDuckTypers[kind] + return duckTyper ? duckTyper.eval(parsedSection) : true + }) + } ) } @@ -192,7 +207,9 @@ export class SharedConfigProfileStore implements ProfileStore { // If setting not passed then preserve setting in file as-is value = value?.toString().trim() if (value === undefined || value === null || value === '') { - Object.hasOwn(parsedSection, name) && delete parsedSection[name] + if (Object.hasOwn(parsedSection, name)) { + delete parsedSection[name] + } } else { if (controlCharsRegex.test(value)) { throwAwsError(`Setting [${name}] cannot contain control characters.`) diff --git a/server/aws-lsp-identity/src/sso/cache/refreshingSsoCache.ts b/server/aws-lsp-identity/src/sso/cache/refreshingSsoCache.ts index d642f0a36b..197fd1680b 100644 --- a/server/aws-lsp-identity/src/sso/cache/refreshingSsoCache.ts +++ b/server/aws-lsp-identity/src/sso/cache/refreshingSsoCache.ts @@ -7,13 +7,11 @@ import { throwOnInvalidClientName, UpdateSsoTokenFromCreateToken, throwOnInvalidSsoSessionName, + SendSsoTokenChanged, } from '../utils' -import { RaiseSsoTokenChanged } from '../../language-server/ssoTokenAutoRefresher' import { CreateTokenCommandOutput, InvalidGrantException } from '@aws-sdk/client-sso-oidc' import { AwsError, Observability } from '@aws/lsp-core' - -export const refreshWindowMillis: number = 5 * 60 * 1000 -export const retryCooldownWindowMillis: number = 30000 +import { refreshWindowMillis, retryCooldownWindowMillis } from '../../language-server/autoRefresher' interface SsoTokenDetail { lastRefreshMillis: number @@ -24,7 +22,7 @@ export class RefreshingSsoCache implements SsoCache { constructor( private readonly next: SsoCache, - private readonly raiseSsoTokenChanged: RaiseSsoTokenChanged, + private readonly raiseSsoTokenChanged: SendSsoTokenChanged, private readonly observability: Observability ) {} @@ -128,14 +126,18 @@ export class RefreshingSsoCache implements SsoCache { // Current time is before start of refresh window? Just return it const refreshAfterMillis = accessTokenExpiresAtMillis - refreshWindowMillis if (nowMillis < refreshAfterMillis) { - this.observability.logging.log('SSO token before refresh window. Returning current SSO token.') + this.observability.logging.log( + 'SSO token expiration is before refresh window. Returning current SSO token.' + ) return ssoToken } // Last refresh attempt was less than the retry window? Just return it const retryAfterMillis = ssoTokenDetail.lastRefreshMillis + retryCooldownWindowMillis if (nowMillis < retryAfterMillis) { - this.observability.logging.log('SSO token in retry cooldown window. Returning current SSO token.') + this.observability.logging.log( + 'SSO token expiration is in retry cooldown window. Returning current SSO token.' + ) return ssoToken } } diff --git a/server/aws-lsp-identity/src/language-server/ssoTokenAutoRefresher.test.ts b/server/aws-lsp-identity/src/sso/ssoTokenAutoRefresher.test.ts similarity index 100% rename from server/aws-lsp-identity/src/language-server/ssoTokenAutoRefresher.test.ts rename to server/aws-lsp-identity/src/sso/ssoTokenAutoRefresher.test.ts diff --git a/server/aws-lsp-identity/src/language-server/ssoTokenAutoRefresher.ts b/server/aws-lsp-identity/src/sso/ssoTokenAutoRefresher.ts similarity index 54% rename from server/aws-lsp-identity/src/language-server/ssoTokenAutoRefresher.ts rename to server/aws-lsp-identity/src/sso/ssoTokenAutoRefresher.ts index 477d5c901f..4a3863129e 100644 --- a/server/aws-lsp-identity/src/language-server/ssoTokenAutoRefresher.ts +++ b/server/aws-lsp-identity/src/sso/ssoTokenAutoRefresher.ts @@ -1,30 +1,18 @@ -import { SsoSession, SsoTokenChangedParams } from '@aws/language-server-runtimes/protocol' -import { RefreshingSsoCache, refreshWindowMillis, retryCooldownWindowMillis } from '../sso/cache/refreshingSsoCache' -import { throwOnInvalidClientName, throwOnInvalidSsoSession, throwOnInvalidSsoSessionName } from '../sso/utils' +import { SsoSession } from '@aws/language-server-runtimes/protocol' +import { RefreshingSsoCache } from './cache/refreshingSsoCache' +import { throwOnInvalidClientName, throwOnInvalidSsoSession, throwOnInvalidSsoSessionName } from './utils' import { MetricEvent } from '@aws/language-server-runtimes/server-interface' -import { normalizeSettingList } from './profiles/profileService' +import { normalizeSettingList } from '../language-server/profiles/profileService' import { __ServiceException } from '@aws-sdk/client-sso-oidc/dist-types/models/SSOOIDCServiceException' import { AwsError, Observability } from '@aws/lsp-core' +import { AutoRefresher, invalidDelay } from '../language-server/autoRefresher' -const bufferedRefreshWindowMillis = refreshWindowMillis * 0.95 -const bufferedRetryCooldownWindowMillis = retryCooldownWindowMillis * 1.05 -const maxRefreshJitterMillis = 10000 -const maxRetryCooldownJitterMillis = 3000 - -export type RaiseSsoTokenChanged = (params: SsoTokenChangedParams) => void - -export class SsoTokenAutoRefresher implements Disposable { - private readonly timeouts: Record = {} - +export class SsoTokenAutoRefresher extends AutoRefresher { constructor( private readonly ssoCache: RefreshingSsoCache, - private readonly observability: Observability - ) {} - - [Symbol.dispose](): void { - for (const ssoSessionName of Object.keys(this.timeouts)) { - this.unwatch(ssoSessionName) - } + observability: Observability + ) { + super(observability) } async watch(clientName: string, ssoSession: SsoSession): Promise { @@ -46,29 +34,12 @@ export class SsoTokenAutoRefresher implements Disposable { return } - const nowMillis = Date.now() - const accessTokenExpiresAtMillis = Date.parse(ssoToken.expiresAt) - let delayMillis: number - - if (nowMillis < accessTokenExpiresAtMillis - refreshWindowMillis) { - // Before refresh window, schedule to run in refresh window with jitter - delayMillis = accessTokenExpiresAtMillis - bufferedRefreshWindowMillis - nowMillis - delayMillis += Math.random() * maxRefreshJitterMillis // Jitter to mitigate race conditions - } else if ( - accessTokenExpiresAtMillis - refreshWindowMillis < nowMillis && - nowMillis < accessTokenExpiresAtMillis - ) { - // In refresh window with time for a retry - delayMillis = bufferedRetryCooldownWindowMillis - delayMillis += Math.random() * maxRetryCooldownJitterMillis // Jitter to mitigate race conditions - } else { - // Otherwise, expired - this.observability.logging.log('SSO token has expired and will not be auto-refreshed.') - return + // Refresh timeout if delay is valid + const delayMillis = this.getDelay(ssoToken.expiresAt) + if (delayMillis !== invalidDelay) { + this.observability.logging.log(`Auto-refreshing SSO token in ${delayMillis} milliseconds.`) + this.timeouts[ssoSession.name] = setTimeout(this.watch.bind(this, clientName, ssoSession), delayMillis) } - - this.observability.logging.log(`Auto-refreshing SSO token in ${delayMillis} milliseconds.`) - this.timeouts[ssoSession.name] = setTimeout(this.watch.bind(this, clientName, ssoSession), delayMillis) } catch (e) { emitMetric(e, ssoSession) diff --git a/server/aws-lsp-identity/src/sso/utils.ts b/server/aws-lsp-identity/src/sso/utils.ts index fe068dc796..38c6846b17 100644 --- a/server/aws-lsp-identity/src/sso/utils.ts +++ b/server/aws-lsp-identity/src/sso/utils.ts @@ -5,6 +5,7 @@ import { ShowMessageRequestParams, SsoSession, Lsp, + SsoTokenChangedParams, } from '@aws/language-server-runtimes/server-interface' import { CreateTokenCommandOutput, SSOOIDC, SSOOIDCClientConfig } from '@aws-sdk/client-sso-oidc' import { SsoClientRegistration } from './cache' @@ -36,7 +37,7 @@ export function getSsoOidc(ssoRegion: string): SSOOIDC & Disposable { } export function throwOnInvalidClientName(clientName?: string): asserts clientName is string { - if (!clientName?.trim().length) { + if (typeof clientName?.trim !== 'function' || !clientName?.trim().length) { throw new AwsError(`Client name [${clientName}] is invalid.`, AwsErrorCodes.E_INVALID_SSO_CLIENT) } } @@ -57,7 +58,7 @@ export function throwOnInvalidClientRegistration( } export function throwOnInvalidSsoSessionName(ssoSessionName?: string): asserts ssoSessionName is string { - if (!ssoSessionName?.trim()) { + if (typeof ssoSessionName?.trim !== 'function' || !ssoSessionName?.trim()) { throw new AwsError('SSO session name is invalid.', AwsErrorCodes.E_INVALID_SSO_SESSION) } } @@ -111,17 +112,19 @@ export function UpdateSsoTokenFromCreateToken( export type ShowUrl = (url: URL) => void export type ShowMessageRequest = (params: ShowMessageRequestParams) => Promise export type ShowProgress = Lsp['sendProgress'] +export type SendSsoTokenChanged = (params: SsoTokenChangedParams) => void +export type SsoHandlers = { + showUrl: ShowUrl + showMessageRequest: ShowMessageRequest + showProgress: ShowProgress + // Add `showMsg: ShowMessage` if needed. +} export type SsoFlowParams = { clientName: string clientRegistration: SsoClientRegistration ssoSession: SsoSession - handlers: { - showUrl: ShowUrl - showMessageRequest: ShowMessageRequest - showProgress: ShowProgress - // Add `showMsg: ShowMessage` if needed. - } + handlers: SsoHandlers token: CancellationToken observability: Observability } diff --git a/server/aws-lsp-identity/src/sts/cache/fileSystemStsCache.test.ts b/server/aws-lsp-identity/src/sts/cache/fileSystemStsCache.test.ts new file mode 100644 index 0000000000..c686ad8cd1 --- /dev/null +++ b/server/aws-lsp-identity/src/sts/cache/fileSystemStsCache.test.ts @@ -0,0 +1,197 @@ +// eslint-disable-next-line @typescript-eslint/no-require-imports +import mock = require('mock-fs') +import { FileSystemStsCache, getStsCredentialFilepath } from './fileSystemStsCache' +import { expect, use } from 'chai' +import { DirectoryItems } from 'mock-fs/lib/filesystem' +import { Logging, IamCredentials, Telemetry } from '@aws/language-server-runtimes/server-interface' +import { access } from 'fs/promises' +import * as fs from 'fs' +import { StubbedInstance, stubInterface } from 'ts-sinon' +import { Observability } from '@aws/lsp-core' + +// eslint-disable-next-line +use(require('chai-as-promised')) + +let sut: FileSystemStsCache + +let observability: StubbedInstance + +const id: string = 'someid' + +const credential: IamCredentials = { + accessKeyId: 'someaccesskeyid', + secretAccessKey: 'somesecretaccesskey', + sessionToken: 'somesessiontoken', + expiration: new Date(Date.now() + 60 * 60 * 1000), +} + +function setupTest(args?: { id?: string; credential?: IamCredentials }): void { + // Just for sanity, safe to call restore if mock not currently active + mock.restore() + + args = { ...{ id, credential }, ...args } + + const mockConfig: DirectoryItems = {} + mockConfig[getStsCredentialFilepath(args.id!)] = JSON.stringify({ + Credentials: { + AccessKeyId: credential.accessKeyId, + SecretAccessKey: credential.secretAccessKey, + SessionToken: credential.sessionToken, + Expiration: credential.expiration, + }, + }) + + mock(mockConfig) +} + +function expectFileExists(filename: string): Chai.Assertion { + return expect(access(filename, fs.constants.F_OK)) +} + +describe('FileSystemStsCache', () => { + beforeEach(() => { + observability = stubInterface() + observability.logging = stubInterface() + observability.telemetry = stubInterface() + + sut = new FileSystemStsCache(observability) + }) + + afterEach(() => { + mock.restore() + }) + + it('removeStsCredential deletes a valid credential', async () => { + const filename = getStsCredentialFilepath(id) + setupTest() + + await expectFileExists(filename).to.not.be.rejectedWith() + + await sut.removeStsCredential(id) + + await expectFileExists(filename).to.be.rejectedWith() + }) + + it('removeStsCredential does nothing on invalid/non-existent credential', async () => { + const filename = getStsCredentialFilepath(id) + setupTest() + + await expectFileExists(filename).to.not.be.rejectedWith() + + await sut.removeStsCredential('non-existent credential') + + await expectFileExists(filename).to.not.be.rejectedWith() + }) + + it('removeStsCredential throws on invalid id', async () => { + await expect(sut.removeStsCredential(' ')).to.be.rejectedWith() + }) + + it('getStsCredential returns valid credential', async () => { + setupTest() + + const actual = await sut.getStsCredential(id) + + expect(actual).to.not.be.null.and.not.undefined + expect(actual?.accessKeyId).to.equal(credential.accessKeyId) + expect(actual?.secretAccessKey).to.equal(credential.secretAccessKey) + expect(actual?.sessionToken).to.equal(credential.sessionToken) + expect(actual?.expiration?.toISOString()).to.equal(credential.expiration?.toISOString()) + }) + + it('getStsCredential returns undefined when file does not exist', async () => { + setupTest() + + const actual = await sut.getStsCredential('does not exist') + + expect(actual).to.be.undefined + }) + + it('getStsCredential returns undefined on invalid credential', async () => { + setupTest({ id: 'invalid-id', credential: {} as IamCredentials }) + + const actual = await sut.getStsCredential(id) + + expect(actual).to.be.undefined + }) + + it('getStsCredential returns undefined on expired credential', async () => { + setupTest({ + id: 'invalid-id', + credential: { + accessKeyId: 'newaccesskeyid', + secretAccessKey: 'newsecretaccesskey', + sessionToken: 'newsessiontoken', + expiration: new Date(Date.now() - 60 * 60 * 1000), + } as IamCredentials, + }) + + const actual = await sut.getStsCredential(id) + + expect(actual).to.be.undefined + }) + + it('setStsCredential writes new valid credential', async () => { + setupTest() + await sut.setStsCredential(id, credential) + + const actual = await sut.getStsCredential(id) + + expect(actual).to.not.be.null.and.not.undefined + expect(actual?.accessKeyId).to.equal(credential.accessKeyId) + expect(actual?.secretAccessKey).to.equal(credential.secretAccessKey) + expect(actual?.sessionToken).to.equal(credential.sessionToken) + expect(actual?.expiration?.toISOString()).to.equal(credential.expiration?.toISOString()) + }) + + it('setStsCredential writes new valid credential when ~/.aws does not exist', async () => { + mock.restore() + mock({}) + + await sut.setStsCredential(id, credential) + + const actual = await sut.getStsCredential(id) + + expect(actual).to.not.be.null.and.not.undefined + expect(actual?.accessKeyId).to.equal(credential.accessKeyId) + expect(actual?.secretAccessKey).to.equal(credential.secretAccessKey) + expect(actual?.sessionToken).to.equal(credential.sessionToken) + expect(actual?.expiration?.toISOString()).to.equal(credential.expiration?.toISOString()) + }) + + it('setStsCredential writes updated existing credential', async () => { + setupTest() + + const newCredential = { + accessKeyId: 'newaccesskeyid', + secretAccessKey: 'newsecretaccesskey', + sessionToken: 'newsessiontoken', + expiration: new Date(Date.now() + 60 * 60 * 1000), + } + await sut.setStsCredential(id, newCredential) + const actual = await sut.getStsCredential(id) + + expect(actual).to.not.be.null.and.not.undefined + expect(actual?.accessKeyId).to.equal(newCredential.accessKeyId) + expect(actual?.secretAccessKey).to.equal(newCredential.secretAccessKey) + expect(actual?.sessionToken).to.equal(newCredential.sessionToken) + expect(actual?.expiration?.toISOString()).to.equal(newCredential.expiration.toISOString()) + }) + + it('setStsCredential returns without error on invalid credential', async () => { + setupTest() + + await sut.setStsCredential(id, {} as IamCredentials) // no throw + }) + + it('setStsCredential returns without error on expired credential', async () => { + setupTest() + + await sut.setStsCredential(id, { + accessKeyId: 'newaccesskeyid', + secretAccessKey: 'newsecretaccesskey', + sessionToken: 'newsessiontoken', + expiration: new Date(Date.now() - 60 * 60 * 1000), + } as IamCredentials) // no throw + }) +}) diff --git a/server/aws-lsp-identity/src/sts/cache/fileSystemStsCache.ts b/server/aws-lsp-identity/src/sts/cache/fileSystemStsCache.ts new file mode 100644 index 0000000000..a11120f232 --- /dev/null +++ b/server/aws-lsp-identity/src/sts/cache/fileSystemStsCache.ts @@ -0,0 +1,136 @@ +import { StsCache } from './stsCache' +import { AwsError, Observability } from '@aws/lsp-core' +import { AwsErrorCodes, IamCredentials, Profile } from '@aws/language-server-runtimes/protocol' +import path, { join } from 'path' +import { mkdir, readFile, unlink, writeFile } from 'fs/promises' +import { getHomeDir } from '@smithy/shared-ini-file-loader' +import { throwOnInvalidCredentialId } from '../../iam/utils' +import { createHash } from 'crypto' + +export class FileSystemStsCache implements StsCache { + constructor(private readonly observability: Observability) {} + + async removeStsCredential(name: string): Promise { + throwOnInvalidCredentialId(name) + + await unlink(getStsCredentialFilepath(name)).catch(reason => this.ignoreDoesNotExistOrThrow(reason)) + } + + async getStsCredential(name: string): Promise { + try { + let credential = await getStsCredentialFromFile(name) + if (!this.isValid(credential)) { + this.observability.logging.log(`Cannot get credential from ${name}: missing fields.`) + return undefined + } + // Ensure expiration is a Date object + if (typeof credential.expiration === 'string') { + credential = { ...credential, expiration: new Date(credential.expiration) } + } + if (this.isExpired(credential)) { + this.observability.logging.log(`Credential from ${name} is expired`) + return undefined + } + return credential + } catch (e) { + this.ignoreDoesNotExistOrThrow(e) + } + } + + async setStsCredential(name: string, credential: IamCredentials): Promise { + if (!this.isValid(credential)) { + this.observability.logging.log('Cannot set credential: missing fields.') + return + } + if (this.isExpired(credential)) { + this.observability.logging.log(`Cannot set credential: expired`) + return undefined + } + + await writeStsObjectToFile(name, credential).catch(reason => { + throw AwsError.wrap(reason, AwsErrorCodes.E_CANNOT_WRITE_SSO_CACHE) + }) + } + + private ignoreDoesNotExistOrThrow(error: unknown): Promise { + // Error codes are consistent across OSes (Windows is converted to libuv error codes) + // https://nodejs.org/api/errors.html#errorerrno + if ((error as SystemError)?.code === 'ENOENT') { + return Promise.resolve(undefined) + } + + this.observability.logging.log('Cannot read STS cache.') + throw AwsError.wrap(error as Error, AwsErrorCodes.E_CANNOT_READ_SSO_CACHE) + } + + private isValid(credential: IamCredentials): boolean { + return ( + credential.accessKeyId !== undefined && + credential.secretAccessKey !== undefined && + credential.sessionToken !== undefined && + credential.expiration !== undefined + ) + } + + private isExpired(credential: IamCredentials): boolean { + if (credential.expiration === undefined) { + return false + } + return Date.now() >= credential.expiration.getTime() + } +} + +export function convertProfileToId(profile: Profile) { + const key = JSON.stringify({ + RoleArn: profile.settings?.role_arn, + RoleSessionName: profile.settings?.role_session_name, + SerialNumber: profile.settings?.mfa_serial, + }) + return createHash('sha1').update(key).digest('hex') +} + +// Based on: +// https://github.com/smithy-lang/smithy-typescript/blob/main/packages/shared-ini-file-loader/src/getSSOTokenFilepath.ts +export function getStsCredentialFilepath(id: string): string { + return join(getHomeDir(), '.aws', 'flare', 'cache', `${id}.json`) +} + +// Based on: +// https://github.com/smithy-lang/smithy-typescript/blob/main/packages/shared-ini-file-loader/src/getSSOTokenFromFile.ts +async function getStsCredentialFromFile(id: string): Promise { + const stsCredentialFilepath = getStsCredentialFilepath(id) + const text = await readFile(stsCredentialFilepath, 'utf8') + const json = JSON.parse(text) + return { + accessKeyId: json.Credentials.AccessKeyId, + secretAccessKey: json.Credentials.SecretAccessKey, + sessionToken: json.Credentials.SessionToken, + expiration: json.Credentials.Expiration, + } as IamCredentials +} + +// Based on: +// https://github.com/aws/aws-sdk-js-v3/blob/6e61f0e78ff7a9e3b1f2cd651bde5fc656d85ba9/packages/token-providers/src/writeSSOTokenToFile.ts +async function writeStsObjectToFile(id: string, credentials: IamCredentials): Promise { + const filepath = getStsCredentialFilepath(id) + await mkdir(path.dirname(filepath), { mode: 0o755, recursive: true }) + const json = JSON.stringify( + { + Credentials: { + AccessKeyId: credentials.accessKeyId, + SecretAccessKey: credentials.secretAccessKey, + SessionToken: credentials.sessionToken, + Expiration: credentials.expiration, + }, + }, + null, + 2 + ) + return await writeFile(filepath, json, { encoding: 'utf-8', flush: true, mode: 0o600 }) +} + +// Minimal declaration of SystemError (no node type declaration for it) to access code property +// https://nodejs.org/api/errors.html#class-systemerror +interface SystemError { + code: string +} diff --git a/server/aws-lsp-identity/src/sts/cache/stsCache.ts b/server/aws-lsp-identity/src/sts/cache/stsCache.ts new file mode 100644 index 0000000000..d89e8dbc4e --- /dev/null +++ b/server/aws-lsp-identity/src/sts/cache/stsCache.ts @@ -0,0 +1,7 @@ +import { IamCredentials } from '@aws/language-server-runtimes/protocol' + +export interface StsCache { + getStsCredential(name: string): Promise + setStsCredential(name: string, credentials: IamCredentials): Promise + removeStsCredential(name: string): Promise +} diff --git a/server/aws-lsp-identity/src/sts/stsAutoRefresher.test.ts b/server/aws-lsp-identity/src/sts/stsAutoRefresher.test.ts new file mode 100644 index 0000000000..ec080b6ba1 --- /dev/null +++ b/server/aws-lsp-identity/src/sts/stsAutoRefresher.test.ts @@ -0,0 +1,124 @@ +import { expect, use } from 'chai' +import { StsAutoRefresher } from './stsAutoRefresher' +import { StubbedInstance, stubInterface } from 'ts-sinon' +import { restore, spy } from 'sinon' +import { AwsErrorCodes, IamCredentials, Logging, Telemetry } from '@aws/language-server-runtimes/server-interface' +import { AwsError, Observability } from '@aws/lsp-core' +import { FileSystemStsCache } from './cache/fileSystemStsCache' + +// eslint-disable-next-line +use(require('chai-as-promised')) + +let observability: StubbedInstance + +const profileName = 'someprofile' +const now = Date.now() + +function createStsCredential(expiresAsOffsetMillis: number): IamCredentials { + return { + accessKeyId: 'someaccesskeyid', + secretAccessKey: 'somesecretaccesskey', + sessionToken: 'somesessiontoken', + expiration: new Date(now + expiresAsOffsetMillis), + } satisfies IamCredentials +} + +function refreshStsCredential(): Promise { + return Promise.resolve({ + accessKeyId: 'newaccesskeyid', + secretAccessKey: 'newsecretaccesskey', + sessionToken: 'newsessiontoken', + expiration: new Date(now + 60 * 60 * 1000 /* 1 hour in relative seconds */), + } satisfies IamCredentials) +} + +function stubStsCache(credential?: IamCredentials): FileSystemStsCache { + return stubInterface({ + getStsCredential: credential + ? Promise.resolve(credential) + : Promise.reject(new AwsError('Test: No STS credential', AwsErrorCodes.E_INVALID_STS_CREDENTIAL)), + }) +} + +describe('StsAutoRefresher', () => { + beforeEach(() => { + observability = stubInterface() + observability.logging = stubInterface() + observability.telemetry = stubInterface() + }) + + afterEach(() => { + restore() + }) + + it('watch does nothing if STS credential is not loaded from cache.', async () => { + const stsCache = stubStsCache() + using sut = new StsAutoRefresher(stsCache, () => {}, observability) + + expect(Object.keys(sut['timeouts']).length).to.equal(0) + + await sut.watch(profileName, refreshStsCredential) + + expect(Object.keys(sut['timeouts']).length).to.equal(0) + }) + + it('watch does nothing if STS credential is expired.', async () => { + const stsCache = stubStsCache(createStsCredential(-10000)) + using sut = new StsAutoRefresher(stsCache, () => {}, observability) + + expect(Object.keys(sut['timeouts']).length).to.equal(0) + + await sut.watch(profileName, refreshStsCredential) + + expect(Object.keys(sut['timeouts']).length).to.equal(0) + }) + + it('watch schedules refresh in refresh window prior to expiration.', async () => { + const setTimeoutSpy = spy(global, 'setTimeout') + + // Before the refresh window + const stsCache = stubStsCache(createStsCredential(60 * 60 * 1000)) + using sut = new StsAutoRefresher(stsCache, () => {}, observability) + + expect(Object.keys(sut['timeouts']).length).to.equal(0) + + await sut.watch(profileName, refreshStsCredential) + + expect(Object.keys(sut['timeouts']).length).to.equal(1) + + expect(setTimeoutSpy.calledOnce).to.be.true + expect(setTimeoutSpy.lastCall.args[1]) + .to.be.greaterThan(55 * 60 * 1000) + .and.lessThan(60 * 60 * 1000) + }) + + it('watch schedules refresh retry in retry window after last attempt.', async () => { + const setTimeoutSpy = spy(global, 'setTimeout') + + // In the refresh window + const stsCache = stubStsCache(createStsCredential(4 * 60 * 1000)) + using sut = new StsAutoRefresher(stsCache, () => {}, observability) + + expect(Object.keys(sut['timeouts']).length).to.equal(0) + + await sut.watch(profileName, refreshStsCredential) + + expect(Object.keys(sut['timeouts']).length).to.equal(1) + + expect(setTimeoutSpy.calledOnce).to.be.true + expect(setTimeoutSpy.lastCall.args[1]) + .to.be.greaterThan(30 * 1000) + .and.lessThan(40 * 1000) + }) + + it('unwatch does nothing if profileName is not watched.', () => { + const stsCache = stubStsCache() + using sut = new StsAutoRefresher(stsCache, () => {}, observability) + + expect(Object.keys(sut['timeouts']).length).to.equal(0) + + sut.unwatch(refreshStsCredential.name) + + expect(Object.keys(sut['timeouts']).length).to.equal(0) + }) +}) diff --git a/server/aws-lsp-identity/src/sts/stsAutoRefresher.ts b/server/aws-lsp-identity/src/sts/stsAutoRefresher.ts new file mode 100644 index 0000000000..e5610a8c7e --- /dev/null +++ b/server/aws-lsp-identity/src/sts/stsAutoRefresher.ts @@ -0,0 +1,84 @@ +import { StsCache } from './cache/stsCache' +import { Observability } from '@aws/lsp-core' +import { AutoRefresher, invalidDelay } from '../language-server/autoRefresher' +import { IamCredentials, StsCredentialChangedKind } from '@aws/language-server-runtimes/protocol' +import { SendStsCredentialChanged } from '../iam/utils' + +interface StsCredentialDetail { + lastRefreshMillis: number +} + +export class StsAutoRefresher extends AutoRefresher { + private readonly stsCredentialDetails: Record = {} + + constructor( + private readonly stsCache: StsCache, + private readonly raiseStsCredentialChanged: SendStsCredentialChanged, + observability: Observability + ) { + super(observability) + } + + async watch(iamCredentialId: string, refreshCallback: () => Promise): Promise { + try { + this.unwatch(iamCredentialId) + + const credential = await this.stsCache.getStsCredential(iamCredentialId).catch(_ => undefined) + + if (!credential?.expiration) { + this.observability.logging.log( + 'STS credentials do not exist or have no expiration, will not be auto-refreshed.' + ) + return + } + + // Get or create StsCredentialDetail + const stsCredentialDetail = + this.stsCredentialDetails[iamCredentialId] ?? + (this.stsCredentialDetails[iamCredentialId] = { lastRefreshMillis: 0 }) + + const delayMillis = this.getDelay(credential.expiration.toISOString()) + if (delayMillis !== invalidDelay) { + this.observability.logging.info(`Auto-refreshing STS credentials in ${delayMillis} milliseconds.`) + this.timeouts[iamCredentialId] = setTimeout(async () => { + try { + // Update last refresh attempt time (matching SSO pattern) + stsCredentialDetail.lastRefreshMillis = Date.now() + + // Passing refresh function into here is easier than refreshing from STS cache + const newCredentials = await refreshCallback() + this.observability.logging.log(`Generated new STS credentials`) + await this.stsCache.setStsCredential(iamCredentialId, newCredentials) + + // Continue watching with the new credentials (allows multiple refreshes) + this.watch(iamCredentialId, refreshCallback) + + this.raiseStsCredentialChanged({ + kind: StsCredentialChangedKind.Refreshed, + stsCredentialId: iamCredentialId, + }) + } catch (error) { + this.observability.logging.log(`Failed to refresh STS credentials: ${error}`) + + // On error, continue watching to retry later (matching SSO pattern) + this.watch(iamCredentialId, refreshCallback) + } + }, delayMillis) + } + } catch (e) { + this.observability.logging.log(`Error setting up STS auto-refresh: ${e}`) + throw e + } + } + + unwatch(stsSessionName: string): void { + const timeout = this.timeouts[stsSessionName] + if (timeout) { + clearTimeout(timeout) + delete this.timeouts[stsSessionName] + // Also clean up the credential detail + delete this.stsCredentialDetails[stsSessionName] + this.observability.logging.log('STS credentials unwatched and will not be auto-refreshed.') + } + } +}