diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 68e055e6886..06f87dae3d4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,3 +2,4 @@ packages/core/src/codewhisperer/ @aws/codewhisperer-team packages/core/src/amazonqFeatureDev/ @aws/earlybird packages/core/src/awsService/accessanalyzer/ @aws/access-analyzer +packages/core/src/awsService/cloudformation/ @aws/cfn-dev-productivity diff --git a/.github/workflows/lintcommit.js b/.github/workflows/lintcommit.js index 4f329223eef..47e194653a3 100644 --- a/.github/workflows/lintcommit.js +++ b/.github/workflows/lintcommit.js @@ -57,6 +57,7 @@ const scopes = new Set([ 'telemetry', 'toolkit', 'ui', + 'sagemakerunifiedstudio', ]) void scopes diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index da8d0c6ea54..97cb46fd71f 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -181,6 +181,67 @@ jobs: with: run: npm run testWeb + cloudformation-integ: + needs: lint-commits + name: CloudFormation LSP E2E Tests + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + node-version: [18.x] + vscode-version: [stable] + env: + VSCODE_TEST_VERSION: ${{ matrix.vscode-version }} + NODE_OPTIONS: '--max-old-space-size=8192' + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Setup CloudFormation LSP + shell: bash + run: bash packages/core/src/testE2E/cloudformation/setup-local-lsp.sh + - run: npm ci + - name: Run CloudFormation E2E Tests (Unix) + if: runner.os != 'Windows' + uses: coactions/setup-xvfb@v1 + with: + run: npm run testE2ECfn -w packages/toolkit + - name: Run CloudFormation E2E Tests (Windows) + if: runner.os == 'Windows' + run: npm run testE2ECfn -w packages/toolkit + - name: Print Extension Logs + if: failure() + shell: bash + run: | + echo "=== AWS Toolkit Extension Logs ===" + find packages/toolkit/.vscode-test/user-data/logs -name "*.log" -type f 2>/dev/null | while read logfile; do + echo "--- $logfile ---" + cat "$logfile" || echo "Could not read log file" + done || echo "No extension logs found" + + echo "" + echo "=== CloudFormation LSP Server Logs ===" + echo "LSP Path: $__CLOUDFORMATIONLSP_PATH" + if [ -n "$__CLOUDFORMATIONLSP_PATH" ]; then + LSP_LOG_DIR="$__CLOUDFORMATIONLSP_PATH/.aws-cfn-storage/logs" + echo "Checking directory: $LSP_LOG_DIR" + if [ -d "$LSP_LOG_DIR" ]; then + find "$LSP_LOG_DIR" -name "*.log" -type f 2>/dev/null | while read logfile; do + echo "--- $logfile ---" + cat "$logfile" || echo "Could not read log file" + done + else + echo "LSP logs directory does not exist: $LSP_LOG_DIR" + echo "Contents of LSP path:" + ls -la "$__CLOUDFORMATIONLSP_PATH" 2>/dev/null || echo "Cannot list LSP path" + fi + else + echo "Environment variable __CLOUDFORMATIONLSP_PATH is not set" + fi + windows: needs: lint-commits name: test Windows diff --git a/.github/workflows/setup-release-candidate.yml b/.github/workflows/setup-release-candidate.yml index 8a96e757fae..390669d22af 100644 --- a/.github/workflows/setup-release-candidate.yml +++ b/.github/workflows/setup-release-candidate.yml @@ -31,6 +31,12 @@ jobs: run: | echo "BRANCH_NAME=release/rc-$(date +%Y%m%d)" >> $GITHUB_OUTPUT + - name: Install dependencies + run: npm ci + + - name: Generate license attribution + run: npm run scan-licenses + - name: Create RC Branch env: BRANCH_NAME: ${{ steps.branch-name.outputs.BRANCH_NAME }} @@ -41,5 +47,10 @@ jobs: # Create RC branch from specified commit git checkout -b $BRANCH_NAME + # Add generated license files + git add LICENSE-THIRD-PARTY + # If there are no changes, then we don't need a new attribution commit + git commit -m "Update third-party license attribution for $BRANCH_NAME" || true + # Push RC branch git push origin $BRANCH_NAME diff --git a/.gitignore b/.gitignore index 8e0c8230ad1..8d9ec981512 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ src.gen/* **/src/codewhisperer/client/codewhispererclient.d.ts **/src/codewhisperer/client/codewhispereruserclient.d.ts **/src/auth/sso/oidcclientpkce.d.ts +**/src/sagemakerunifiedstudio/shared/client/gluecatalogapi.d.ts +**/src/sagemakerunifiedstudio/shared/client/sqlworkbench.d.ts +**/src/sagemakerunifiedstudio/shared/client/datazonecustomclient.d.ts # Generated by tests **/src/testFixtures/**/bin @@ -59,3 +62,6 @@ packages/*/resources/css/icons.css # Generated by E2E UI Tests packages/amazonq/test/e2e_new/amazonq/resources packages/amazonq/test/e2e_new/amazonq/logs + +# License scanning output +licenses-full.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9992cd16dcf..04e90660dec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -527,6 +527,11 @@ Unlike the user setting overrides, not all of these environment variables have t - `SSMDOCUMENT_LANGUAGESERVER_PORT`: The port the ssm document language server should start debugging on +#### CloudFormation LSP + +- `__CLOUDFORMATIONLSP_PATH`: for aws.dev.cloudformationLsp.path +- `__CLOUDFORMATIONLSP_CLOUDFORMATION_ENDPOINT`: for aws.dev.cloudformationLsp.cloudformationEndpoint + #### CI/Testing - `GITHUB_ACTION`: The name of the current GitHub Action workflow step that is running diff --git a/LICENSE-THIRD-PARTY b/LICENSE-THIRD-PARTY new file mode 100644 index 00000000000..873fd158694 --- /dev/null +++ b/LICENSE-THIRD-PARTY @@ -0,0 +1,8784 @@ +@aws/language-server-runtimes +0.3.5 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + +****************************** + +@aws/language-server-runtimes-types +0.1.61 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + +****************************** + +@opentelemetry/api +1.9.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@opentelemetry/api-logs +0.200.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@opentelemetry/core +2.0.1 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@opentelemetry/exporter-logs-otlp-http +0.200.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@opentelemetry/exporter-metrics-otlp-http +0.200.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@opentelemetry/otlp-exporter-base +0.200.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@opentelemetry/otlp-transformer +0.200.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@opentelemetry/resources +2.0.1 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@opentelemetry/sdk-logs +0.200.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@opentelemetry/sdk-metrics +2.0.1 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@opentelemetry/sdk-trace-base +2.0.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@opentelemetry/semantic-conventions +1.33.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@protobufjs/aspromise +1.1.2 +Copyright (c) 2016, Daniel Wirtz All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of its author, nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +@protobufjs/base64 +1.1.2 +Copyright (c) 2016, Daniel Wirtz All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of its author, nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +@protobufjs/codegen +2.0.4 +Copyright (c) 2016, Daniel Wirtz All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of its author, nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +@protobufjs/eventemitter +1.1.0 +Copyright (c) 2016, Daniel Wirtz All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of its author, nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +@protobufjs/fetch +1.1.0 +Copyright (c) 2016, Daniel Wirtz All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of its author, nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +@protobufjs/float +1.0.2 +Copyright (c) 2016, Daniel Wirtz All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of its author, nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +@protobufjs/inquire +1.1.0 +Copyright (c) 2016, Daniel Wirtz All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of its author, nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +@protobufjs/path +1.1.2 +Copyright (c) 2016, Daniel Wirtz All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of its author, nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +@protobufjs/pool +1.1.0 +Copyright (c) 2016, Daniel Wirtz All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of its author, nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +@protobufjs/utf8 +1.1.0 +Copyright (c) 2016, Daniel Wirtz All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of its author, nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +@smithy/abort-controller +4.0.2 +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +****************************** + +@smithy/node-http-handler +4.0.4 +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +****************************** + +@smithy/protocol-http +5.1.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@smithy/querystring-builder +4.0.2 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@smithy/types +4.2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +@smithy/util-uri-escape +4.0.0 +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +****************************** + +@types/node +22.8.4 + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +****************************** + +ajv +8.17.1 +The MIT License (MIT) + +Copyright (c) 2015-2021 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +****************************** + +ansi-colors +4.1.1 +The MIT License (MIT) + +Copyright (c) 2015-present, Brian Woodward. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +ansi-gray +0.1.1 +The MIT License (MIT) + +Copyright (c) <%= year() %>, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +ansi-regex +5.0.1 +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +ansi-styles +4.3.0 +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +ansi-wrap +0.1.0 +The MIT License (MIT) + +Copyright (c) 2015, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +aproba +1.2.0 +Copyright (c) 2015, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +****************************** + +are-we-there-yet +1.1.7 +Copyright (c) 2015, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +balanced-match +1.0.2 +(MIT) + +Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +base64-js +1.5.1 +The MIT License (MIT) + +Copyright (c) 2014 Jameson Little + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +bl +4.1.0 +The MIT License (MIT) +===================== + +Copyright (c) 2013-2019 bl contributors +---------------------------------- + +*bl contributors listed at * + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +brace-expansion +1.1.11 +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +buffer +5.7.1 +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh, and other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +chownr +1.1.4 +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +cliui +8.0.1 +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +clone +2.1.2 +Copyright © 2011-2015 Paul Vorbach + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +clone-buffer +1.0.0 +The MIT License (MIT) + +Copyright (c) 2016 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +clone-stats +1.0.0 +## The MIT License (MIT) ## + +Copyright (c) 2014 Hugh Kennedy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +cloneable-readable +1.1.3 +The MIT License (MIT) + +Copyright (c) 2016 Matteo Collina + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +code-point-at +1.1.0 +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +color-convert +2.0.1 +Copyright (c) 2011-2016 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +****************************** + +color-name +1.1.4 +The MIT License (MIT) +Copyright (c) 2015 Dmitry Ivanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +****************************** + +color-support +1.1.3 +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +concat-map +0.0.1 +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +console-control-strings +1.1.0 +Copyright (c) 2014, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +core-util-is +1.0.3 +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +****************************** + +decompress-response +4.2.1 +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +deep-extend +0.6.0 +The MIT License (MIT) + +Copyright (c) 2013-2018, Viacheslav Lotsmanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +delegates +1.0.0 +Copyright (c) 2015 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +detect-libc +1.0.3 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +duplexer +0.1.2 +license: MIT +authors: Raynos + +****************************** + +emoji-regex +8.0.0 +license: MIT +authors: Mathias Bynens + +****************************** + +end-of-stream +1.4.4 +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +****************************** + +escalade +3.1.2 +MIT License + +Copyright (c) Luke Edwards (lukeed.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +event-stream +3.3.5 +license: MIT +authors: Dominic Tarr (http://bit.ly/dominictarr) + +****************************** + +expand-template +2.0.3 +The MIT License (MIT) + +Copyright (c) 2018 Lars-Magnus Skog + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +****************************** + +fancy-log +1.3.3 +The MIT License (MIT) + +Copyright (c) 2014, 2015, 2018 Blaine Bublitz and Eric Schoffstall + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +****************************** + +fast-deep-equal +3.1.3 +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +fast-uri +3.0.6 +Copyright (c) 2021 The Fastify Team +Copyright (c) 2011-2021, Gary Court until https://github.com/garycourt/uri-js/commit/a1acf730b4bba3f1097c9f52e7d9d3aba8cdcaae +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of any contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * * * + +The complete list of contributors can be found at: +- https://github.com/garycourt/uri-js/graphs/contributors + +****************************** + +from +0.1.7 +Apache License, Version 2.0 + +Copyright (c) 2011 Dominic Tarr + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +****************************** + +fs-constants +1.0.0 +The MIT License (MIT) + +Copyright (c) 2018 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +fs.realpath +1.0.0 +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---- + +This library bundles a version of the `fs.realpath` and `fs.realpathSync` +methods from Node.js v0.10 under the terms of the Node.js MIT license. + +Node's license follows, also included at the header of `old.js` which contains +the licensed code: + + Copyright Joyent, Inc. and other Node contributors. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + +****************************** + +gauge +2.7.4 +Copyright (c) 2014, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +get-caller-file +2.0.5 +ISC License (ISC) +Copyright 2018 Stefan Penner + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +github-from-package +0.0.0 +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +glob +7.2.3 +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +## Glob Logo + +Glob's logo created by Tanya Brassie , licensed +under a Creative Commons Attribution-ShareAlike 4.0 International License +https://creativecommons.org/licenses/by-sa/4.0/ + + +****************************** + +has-unicode +2.0.1 +Copyright (c) 2014, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +****************************** + +hpagent +1.2.0 +MIT License + +Copyright (c) 2020 Tomas Della Vedova + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +iconv-lite +0.6.3 +Copyright (c) 2011 Alexander Shtuchkin + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +****************************** + +ieee754 +1.1.13 +Copyright 2008 Fair Oaks Labs, Inc. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +inflight +1.0.6 +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +inherits +2.0.4 +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + + +****************************** + +ini +1.3.8 +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +is +3.3.0 +(The MIT License) + +Copyright (c) 2013 Enrico Marino +Copyright (c) 2014 Enrico Marino and Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +is-electron +2.2.2 +The MIT License (MIT) + +Copyright (c) 2016-2018 Cheton Wu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +is-fullwidth-code-point +3.0.0 +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +isarray +1.0.0 +license: MIT +authors: Julian Gruber + +****************************** + +jaro-winkler +0.2.8 +The MIT License (MIT) + +Copyright (c) 2015 Jordan Thomas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +****************************** + +jose +5.10.0 +The MIT License (MIT) + +Copyright (c) 2018 Filip Skokan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +json-schema-traverse +1.0.0 +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +long +5.3.1 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +****************************** + +mac-ca +3.1.1 +BSD 3-Clause License + +Copyright (c) 2018, José F. Romaniello +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +****************************** + +make-dir +1.3.0 +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +map-stream +0.0.7 +license: MIT +authors: Dominic Tarr (http://dominictarr.com) + +****************************** + +mimic-response +2.1.0 +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +minimatch +3.1.2 +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +minimist +1.2.8 +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +mkdirp-classic +0.5.3 +The MIT License (MIT) + +Copyright (c) 2020 James Halliday (mail@substack.net) and Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +napi-build-utils +1.0.2 +MIT License + +Copyright (c) 2018 inspiredware + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +node-abi +2.30.1 +MIT License + +Copyright (c) 2016 Lukas Geiger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +node-addon-api +3.2.1 +The MIT License (MIT) +===================== + +Copyright (c) 2017 Node.js API collaborators +----------------------------------- + +*Node.js API collaborators listed at * + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +****************************** + +node-forge +1.3.1 +You may use the Forge project under the terms of either the BSD License or the +GNU General Public License (GPL) Version 2. + +The BSD License is recommended for most projects. It is simple and easy to +understand and it places almost no restrictions on what you can do with the +Forge project. + +If the GPL suits your project better you are also free to use Forge under +that license. + +You don't have to do anything special to choose one license or the other and +you don't have to notify anyone which license you are using. You are free to +use this project in commercial projects as long as the copyright header is +left intact. + +If you are a commercial entity and use this set of libraries in your +commercial software then reasonable payment to Digital Bazaar, if you can +afford it, is not required but is expected and would be appreciated. If this +library saves you time, then it's saving you money. The cost of developing +the Forge software was on the order of several hundred hours and tens of +thousands of dollars. We are attempting to strike a balance between helping +the development community while not being taken advantage of by lucrative +commercial entities for our efforts. + +------------------------------------------------------------------------------- +New BSD License (3-clause) +Copyright (c) 2010, Digital Bazaar, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Digital Bazaar, Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------- + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + + +****************************** + +noop-logger +0.1.1 +license: MIT +authors: undefined + +****************************** + +npmlog +4.1.2 +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +number-is-nan +1.0.1 +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +object-assign +4.1.1 +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +once +1.4.0 +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +parse-node-version +1.0.1 +The MIT License (MIT) + +Copyright (c) 2018 Blaine Bublitz and Eric Schoffstall + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +****************************** + +path-is-absolute +1.0.1 +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +pause-stream +0.0.11 +Dual Licensed MIT and Apache 2 + +The MIT License + +Copyright (c) 2013 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + ----------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2013 Dominic Tarr + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +****************************** + +pify +3.0.0 +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +prebuild-install +5.3.6 +The MIT License (MIT) + +Copyright (c) 2015 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +process-nextick-args +2.0.1 +# Copyright (c) 2015 Calvin Metcalf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.** + + +****************************** + +protobufjs +7.4.0 +This license applies to all parts of protobuf.js except those files +either explicitly including or referencing a different license or +located in a directory containing a different LICENSE file. + +--- + +Copyright (c) 2016, Daniel Wirtz All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of its author, nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +Code generated by the command line utilities is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + + +****************************** + +pump +3.0.0 +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +****************************** + +rc +1.2.8 +Apache License, Version 2.0 + +Copyright (c) 2011 Dominic Tarr + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +****************************** + +readable-stream +3.6.2 +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + +****************************** + +registry-js +1.16.1 +MIT License + +Copyright (c) 2017 GitHub Desktop + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +remove-trailing-separator +1.1.0 +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +****************************** + +replace-ext +1.0.1 +The MIT License (MIT) + +Copyright (c) 2014 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +require-directory +2.1.1 +The MIT License (MIT) + +Copyright (c) 2011 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +require-from-string +2.0.2 +The MIT License (MIT) + +Copyright (c) Vsevolod Strukchinsky (github.com/floatdrop) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +rxjs +7.8.2 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +****************************** + +safe-buffer +5.2.1 +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +safer-buffer +2.1.2 +MIT License + +Copyright (c) 2018 Nikita Skovoroda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +sax +1.2.1 +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +==== + +`String.fromCodePoint` by Mathias Bynens used according to terms of MIT +License, as follows: + + Copyright Mathias Bynens + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +semver +5.7.2 +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +set-blocking +2.0.0 +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +signal-exit +3.0.7 +The ISC License + +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +simple-concat +1.0.1 +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +simple-get +3.1.1 +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +source-map +0.6.1 + +Copyright (c) 2009-2011, Mozilla Foundation and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the names of the Mozilla Foundation nor the names of project + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +****************************** + +split +1.0.1 +license: MIT +authors: Dominic Tarr (http://bit.ly/dominictarr) + +****************************** + +stream-combiner +0.2.2 +Copyright (c) 2012 'Dominic Tarr' + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +string-width +4.2.3 +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +string_decoder +1.3.0 +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + + +****************************** + +strip-ansi +6.0.1 +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +strip-json-comments +2.0.1 +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +tar-fs +2.1.1 +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +****************************** + +tar-stream +2.2.0 +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +****************************** + +through +2.3.8 +Apache License, Version 2.0 + +Copyright (c) 2011 Dominic Tarr + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +****************************** + +time-stamp +1.1.0 +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +tslib +2.8.1 +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +****************************** + +tunnel-agent +0.6.0 +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +****************************** + +typescript +4.9.5 +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + +****************************** + +undici +6.21.2 +MIT License + +Copyright (c) Matteo Collina and Undici contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +undici-types +6.19.8 +MIT License + +Copyright (c) Matteo Collina and Undici contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +util-deprecate +1.0.2 +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +vinyl +2.2.1 +The MIT License (MIT) + +Copyright (c) 2013 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +vscode-jsonrpc +8.2.0 +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +vscode-languageserver +9.0.1 +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +vscode-languageserver-protocol +3.17.5 +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +vscode-languageserver-textdocument +1.0.12 +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +vscode-languageserver-types +3.17.5 +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +vscode-nls +5.2.0 +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +vscode-nls-dev +4.0.4 +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +vscode-uri +3.1.0 +The MIT License (MIT) + +Copyright (c) Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +****************************** + +which-pm-runs +1.1.0 +The MIT License (MIT) + +Copyright (c) 2017-2022 Zoltan Kochan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +wide-align +1.1.5 +Copyright (c) 2015, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +****************************** + +win-ca +3.5.1 +MIT License + +Copyright (c) 2020 Stas Ukolov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +wrap-ansi +7.0.0 +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +wrappy +1.0.2 +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +****************************** + +xml2js +0.5.0 +Copyright 2010, 2011, 2012, 2013. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +****************************** + +xmlbuilder +11.0.1 +The MIT License (MIT) + +Copyright (c) 2013 Ozgur Ozcitak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +y18n +5.0.8 +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +****************************** + +yargs +17.7.2 +MIT License + +Copyright 2010 James Halliday (mail@substack.net); Modified work Copyright 2014 Contributors (ben@npmjs.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +****************************** + +yargs-parser +21.1.1 +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md index 39db7a3ac5f..b841f69ec0c 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,22 @@ We want your feedback! - [File an issue](https://github.com/aws/aws-toolkit-vscode/issues/new?labels=bug&template=bug_report.md) - Or [send a pull request](CONTRIBUTING.md)! +## License Scanning + +To generate license reports and attribution documents for third-party dependencies: + +```bash +npm run scan-licenses + +# Or run directly +./scripts/scan-licenses.sh +``` + +This generates: + +- `LICENSE-THIRD-PARTY` - Attribution document for distribution +- `licenses-full.json` - Complete license data + ## License This project and the subprojects within **(AWS Toolkit for Visual Studio Code, Amazon Q for Visual Studio Code)** is distributed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/buildspec/release/00clonerepo.yml b/buildspec/release/00clonerepo.yml deleted file mode 100644 index 3fbf222ce9a..00000000000 --- a/buildspec/release/00clonerepo.yml +++ /dev/null @@ -1,27 +0,0 @@ -version: 0.2 - -env: - variables: - NODE_OPTIONS: '--max-old-space-size=8192' - -phases: - install: - runtime-versions: - nodejs: 16 - - pre_build: - commands: - # Check for implicit env vars passed from the release pipeline. - - test -n "${TOOLKITS_GITHUB_REPO_OWNER}" - - test -n "${TARGET_BRANCH}" - - build: - commands: - - git clone https://github.com/${TOOLKITS_GITHUB_REPO_OWNER}/aws-toolkit-vscode.git aws-toolkit-vscode - # checkout the target branch as we want to commit to it later to update versions - - cd aws-toolkit-vscode && git checkout ${TARGET_BRANCH} - -artifacts: - base-directory: aws-toolkit-vscode - files: - - '**/*' diff --git a/buildspec/release/10changeversion.yml b/buildspec/release/10changeversion.yml deleted file mode 100644 index 2a43a5f515f..00000000000 --- a/buildspec/release/10changeversion.yml +++ /dev/null @@ -1,45 +0,0 @@ -version: 0.2 - -env: - variables: - NODE_OPTIONS: '--max-old-space-size=8192' - -phases: - pre_build: - commands: - - aws codeartifact login --tool npm --domain "$TOOLKITS_CODEARTIFACT_DOMAIN" --domain-owner "$TOOLKITS_ACCOUNT_ID" --repository "$TOOLKITS_CODEARTIFACT_REPO" - - test -n "${TARGET_EXTENSION}" - - install: - runtime-versions: - nodejs: 16 - - build: - commands: - - | - echo "TARGET_EXTENSION=${TARGET_EXTENSION}" - echo "Removing SNAPSHOT from version string" - git config --global user.name "aws-toolkit-automation" - git config --global user.email "<>" - VERSION=$(node -e "console.log(require('./packages/${TARGET_EXTENSION}/package.json').version);" | (IFS="-"; read -r version unused && echo "$version")) - DATE=$(date) - npm version --no-git-tag-version "$VERSION" -w packages/${TARGET_EXTENSION} - # 'createRelease' uses ts-node. - # Ignore broken "postinstall" script in "src.gen/@amzn/codewhisperer-streaming/package.json". - npm install --ignore-scripts ts-node - - | - npm run createRelease -w packages/${TARGET_EXTENSION} - - | - git add packages/${TARGET_EXTENSION}/package.json - git add package-lock.json - git commit -m "Release $VERSION" - echo "tagging commit" - # e.g. amazonq/v1.0.0. Ensure this tag is up to date with 50githubrelease.yml - git tag -a "${TARGET_EXTENSION}/v${VERSION}" -m "${TARGET_EXTENSION} version $VERSION $DATE" - # cleanup - git clean -fxd - git reset HEAD --hard - -artifacts: - files: - - '**/*' diff --git a/buildspec/release/20buildrelease.yml b/buildspec/release/20buildrelease.yml deleted file mode 100644 index 8af4ef5df4f..00000000000 --- a/buildspec/release/20buildrelease.yml +++ /dev/null @@ -1,38 +0,0 @@ -version: 0.2 - -env: - variables: - NODE_OPTIONS: '--max-old-space-size=8192' - -phases: - pre_build: - commands: - - aws codeartifact login --tool npm --domain "$TOOLKITS_CODEARTIFACT_DOMAIN" --domain-owner "$TOOLKITS_ACCOUNT_ID" --repository "$TOOLKITS_CODEARTIFACT_REPO" - - test -n "${TARGET_EXTENSION}" - install: - runtime-versions: - nodejs: 16 - - commands: - - apt-get update - - apt-get install -y libgtk-3-dev libxss1 xvfb - - apt-get install -y libnss3-dev libasound2 - - apt-get install -y libasound2-plugins - build: - commands: - - echo "TARGET_EXTENSION=${TARGET_EXTENSION}" - # --unsafe-perm is needed because we run as root - - npm ci --unsafe-perm - - npm run package -w packages/${TARGET_EXTENSION} - - cp packages/${TARGET_EXTENSION}/package.json ./package.json - - NUM_VSIX=$(ls -1q *.vsix | wc -l) - - | - if [ "$NUM_VSIX" != "1" ]; then - echo "Number of .vsix to release is not exactly 1, it is: ${NUM_VSIX}" - exit 1 - fi - -artifacts: - files: - - '*.vsix' - - package.json diff --git a/buildspec/release/30closegate.yml b/buildspec/release/30closegate.yml deleted file mode 100644 index 618613e782f..00000000000 --- a/buildspec/release/30closegate.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: 0.2 - -phases: - install: - runtime-versions: - nodejs: 16 - - pre_build: - commands: - - STAGE_NAME=Release - - PIPELINE=$(echo $CODEBUILD_INITIATOR | sed -e 's/codepipeline\///') - build: - commands: - - | - aws codepipeline disable-stage-transition \ - --pipeline-name "$PIPELINE" \ - --stage-name "$STAGE_NAME" \ - --transition-type "Inbound" \ - --reason "Disabled by CloseGate (automation)" diff --git a/buildspec/release/35opengate.yml b/buildspec/release/35opengate.yml deleted file mode 100644 index 45362ac14e3..00000000000 --- a/buildspec/release/35opengate.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: 0.2 - -phases: - install: - runtime-versions: - nodejs: 16 - - pre_build: - commands: - - STAGE_NAME=SourceWithGit - - PIPELINE=$(echo $CODEBUILD_INITIATOR | sed -e 's/codepipeline\///') - build: - commands: - - | - aws codepipeline enable-stage-transition \ - --pipeline-name "$PIPELINE" \ - --stage-name "$STAGE_NAME" \ - --transition-type "Inbound" diff --git a/buildspec/release/40pushtogithub.yml b/buildspec/release/40pushtogithub.yml deleted file mode 100644 index a31f34031a3..00000000000 --- a/buildspec/release/40pushtogithub.yml +++ /dev/null @@ -1,46 +0,0 @@ -version: 0.2 - -env: - variables: - NODE_OPTIONS: '--max-old-space-size=8192' - -phases: - install: - runtime-versions: - nodejs: 16 - - pre_build: - commands: - # Check for implicit env vars passed from the release pipeline. - - test -n "${TOOLKITS_GITHUB_REPO_OWNER}" - - test -n "${GITHUB_TOKEN}" - - test -n "${TARGET_EXTENSION}" - - test -n "${TARGET_BRANCH}" - - REPO_URL="https://$GITHUB_TOKEN@github.com/${TOOLKITS_GITHUB_REPO_OWNER}/aws-toolkit-vscode.git" - - build: - commands: - - | - echo "TARGET_EXTENSION=${TARGET_EXTENSION}" - git config --global user.name "aws-toolkit-automation" - git config --global user.email "<>" - git remote add originWithCreds "$REPO_URL" - echo "Adding SNAPSHOT to next version string" - # Increase minor version - npm version --no-git-tag-version minor -w packages/${TARGET_EXTENSION} - VERSION=$(node -e "console.log(require('./packages/${TARGET_EXTENSION}/package.json').version);") - # Append -SNAPSHOT - npm version --no-git-tag-version "${VERSION}-SNAPSHOT" -w packages/${TARGET_EXTENSION} - git add packages/${TARGET_EXTENSION}/package.json - git add package-lock.json - git commit -m "Update version to snapshot version: ${VERSION}-SNAPSHOT" - - | - if [ "$STAGE" != "prod" ]; then - echo "SKIPPED (stage=${STAGE}): 'git push originWithCreds ${TARGET_BRANCH}'" - exit 0 - fi - echo "pushing to github" - git fetch originWithCreds ${TARGET_BRANCH} - git merge --no-edit -m "Merge release into ${TARGET_BRANCH}" FETCH_HEAD - git push originWithCreds --tags - git push originWithCreds ${TARGET_BRANCH} diff --git a/buildspec/release/50githubrelease.yml b/buildspec/release/50githubrelease.yml deleted file mode 100644 index df542cbee14..00000000000 --- a/buildspec/release/50githubrelease.yml +++ /dev/null @@ -1,48 +0,0 @@ -version: 0.2 - -phases: - install: - runtime-versions: - nodejs: 16 - - Commands: - # GitHub recently changed their GPG signing key for their CLI tool - # These are the updated installation instructions: - # https://github.com/cli/cli/blob/trunk/docs/install_linux.md#debian-ubuntu-linux-raspberry-pi-os-apt - - curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg - - chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg - - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null - - apt update - - apt install gh -y - - pre_build: - commands: - # Check for implicit env vars passed from the release pipeline. - - test -n "${TOOLKITS_GITHUB_REPO_OWNER}" - - test -n "${TARGET_EXTENSION}" - - REPO="${TOOLKITS_GITHUB_REPO_OWNER}/aws-toolkit-vscode" - - build: - commands: - - echo "TARGET_EXTENSION=${TARGET_EXTENSION}" - # pull in the build artifacts - - cp -r ${CODEBUILD_SRC_DIR_buildPipeline}/* . - - VERSION=$(node -e "console.log(require('./packages/${TARGET_EXTENSION}/package.json').version);") - - UPLOAD_TARGET=$(ls *.vsix) - - HASH_UPLOAD_TARGET=${UPLOAD_TARGET}.sha384 - - 'HASH=$(sha384sum -b $UPLOAD_TARGET | cut -d" " -f1)' - - echo "Writing hash to $HASH_UPLOAD_TARGET" - - echo $HASH > $HASH_UPLOAD_TARGET - - echo "posting $VERSION with sha384 hash $HASH to GitHub" - - PKG_DISPLAY_NAME=$(grep -m 1 displayName packages/${TARGET_EXTENSION}/package.json | grep -o '[a-zA-z][^\"]\+' | tail -n1) - - RELEASE_MESSAGE="${PKG_DISPLAY_NAME} for VS Code $VERSION" - # Only set amazonq as "latest" release. This ensures https://api.github.com/repos/aws/aws-toolkit-vscode/releases/latest - # consistently points to the amazonq artifact, instead of being "random". - - LATEST="$([ "$TARGET_EXTENSION" = amazonq ] && echo '--latest' || echo '--latest=false' )" - - | - if [ "$STAGE" = "prod" ]; then - # note: the tag arg passed here should match what is in 10changeversion.yml - gh release create "$LATEST" --repo $REPO --title "$PKG_DISPLAY_NAME $VERSION" --notes "$RELEASE_MESSAGE" -- "${TARGET_EXTENSION}/v${VERSION}" "$UPLOAD_TARGET" "$HASH_UPLOAD_TARGET" - else - echo "SKIPPED (stage=${STAGE}): 'gh release create --repo $REPO'" - fi diff --git a/buildspec/release/60publish.yml b/buildspec/release/60publish.yml deleted file mode 100644 index 0141b6e68c2..00000000000 --- a/buildspec/release/60publish.yml +++ /dev/null @@ -1,41 +0,0 @@ -# -# Publishes the release vsix to the marketplace. -# - -version: 0.2 - -phases: - install: - runtime-versions: - nodejs: 20 - commands: - - apt-get update - - apt-get install -y libsecret-1-dev - - pre_build: - commands: - # Check for implicit env vars passed from the release pipeline. - - test -n "${VS_MARKETPLACE_PAT}" - - test -n "${TARGET_EXTENSION}" - - build: - commands: - - echo "TARGET_EXTENSION=${TARGET_EXTENSION}" - # pull in the build artifacts - - cp -r ${CODEBUILD_SRC_DIR_buildPipeline}/* . - - | - UPLOAD_TARGET=$(ls *.vsix) - - | - echo "Publishing to vscode marketplace: $UPLOAD_TARGET" - if [ "$STAGE" != "prod" ]; then - echo "SKIPPED (stage=${STAGE}): 'npx vsce publish --pat xxx --packagePath ${UPLOAD_TARGET}'" - else - npx vsce publish --pat "$VS_MARKETPLACE_PAT" --packagePath "$UPLOAD_TARGET" - fi - - | - echo "Publishing to openvsx marketplace: $UPLOAD_TARGET" - if [ "$STAGE" != "prod" ]; then - echo "SKIPPED (stage=${STAGE}): 'npx --yes ovsx publish --pat xxx "${UPLOAD_TARGET}"'" - else - npx --yes ovsx publish --pat "$OVSX_PAT" "$UPLOAD_TARGET" - fi diff --git a/buildspec/release/70checkmarketplace.yml b/buildspec/release/70checkmarketplace.yml deleted file mode 100644 index 670dd2c7508..00000000000 --- a/buildspec/release/70checkmarketplace.yml +++ /dev/null @@ -1,53 +0,0 @@ -version: 0.2 - -phases: - install: - runtime-versions: - nodejs: 16 - - commands: - - apt update - - apt install -y wget gpg - - curl -sSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg - - install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/ - - sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" > /etc/apt/sources.list.d/vscode.list' - - apt update - - apt install -y code - - pre_build: - commands: - # Check for implicit env vars passed from the release pipeline. - - test -n "${TARGET_EXTENSION}" - - build: - commands: - - VERSION=$(node -e "console.log(require('./packages/${TARGET_EXTENSION}/package.json').version);") - # get extension name, if in beta, use some hard-coded recent version - - | - if [ "${TARGET_EXTENSION}" = "amazonq" ]; then - extension_name="amazonwebservices.amazon-q-vscode" - [ "$STAGE" != "prod" ] && VERSION="1.43.0" || true - elif [ "${TARGET_EXTENSION}" = "toolkit" ]; then - extension_name="amazonwebservices.aws-toolkit-vscode" - [ "$STAGE" != "prod" ] && VERSION="3.42.0" || true - else - echo checkmarketplace: "Unknown TARGET_EXTENSION: ${TARGET_EXTENSION}" - exit 1 - fi - if [ "$STAGE" != "prod" ]; then - echo "checkmarketplace: Non-production stage detected. Installing hardcoded version '${VERSION}'." - fi - # keep installing the desired extension version until successful. Otherwise fail on codebuild timeout (1 hour). - - | - while true; do - code --uninstall-extension "${extension_name}" --no-sandbox --user-data-dir /tmp/vscode - code --install-extension "${extension_name}@${VERSION}" --no-sandbox --user-data-dir /tmp/vscode || true - cur_version=$(code --list-extensions --show-versions --no-sandbox --user-data-dir /tmp/vscode | grep ${extension_name} | cut -d'@' -f2) - if [ "${cur_version}" = "${VERSION}" ]; then - echo "checkmarketplace: Extension ${extension_name} is updated to version '${cur_version}.'" - break - else - echo "checkmarketplace: Expected extension version '${VERSION}' has not been successfully installed. Retrying..." - fi - sleep 120 # Wait for 2 minutes before retrying - done diff --git a/buildspec/release/80notify.yml b/buildspec/release/80notify.yml deleted file mode 100644 index 062895d09d0..00000000000 --- a/buildspec/release/80notify.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: 0.2 - -phases: - install: - runtime-versions: - nodejs: 20 - - pre_build: - commands: - # Check for implicit env vars passed from the release pipeline. - - test -n "${NOTIFY_URL}" - - test -n "${TARGET_EXTENSION}" - - build: - commands: - - echo "TARGET_EXTENSION=${TARGET_EXTENSION}" - - export EXTENSION_NAME=$([ "$TARGET_EXTENSION" = "amazonq" ] && echo "Amazon Q" || echo "AWS Toolkit") - - export VERSION=$(node -e "console.log(require('./packages/${TARGET_EXTENSION}/package.json').version);") - - export CHANGELOG=$(cat packages/${TARGET_EXTENSION}/CHANGELOG.md | perl -ne 'BEGIN{$/="\n\n"} print if $. == 2') - - MESSAGE=$(envsubst < ./buildspec/release/notify.txt | jq -R -s '.') - - echo "Will post message - \n\n${MESSAGE}\n" - - echo "Full command - 'curl -v POST \"[NOTIFY_URL]\" -H \"Content-Type:application/json\" --data \"{\"Content\":${MESSAGE}}\"'" - - | - if [ "$STAGE" != "prod" ]; then - echo "SKIPPED (stage=${STAGE}): curl -v POST ..." - exit 0 - fi - curl -v POST "${NOTIFY_URL}" -H "Content-Type:application/json" --data "{\"Content\":${MESSAGE}}" diff --git a/buildspec/release/notify.txt b/buildspec/release/notify.txt deleted file mode 100644 index 919ee5f4be0..00000000000 --- a/buildspec/release/notify.txt +++ /dev/null @@ -1,6 +0,0 @@ -Released ${EXTENSION_NAME} v${VERSION} for VS Code - -${CHANGELOG} - -Changelog: https://github.com/aws/aws-toolkit-vscode/blob/master/packages/${TARGET_EXTENSION}/CHANGELOG.md -Release Artifact: https://github.com/aws/aws-toolkit-vscode/releases/tag/${TARGET_EXTENSION}/v${VERSION} \ No newline at end of file diff --git a/docs/lsp.md b/docs/lsp.md index 884c7cce378..49a6ad00b87 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -77,26 +77,60 @@ If you want to connect a local version of language-server-runtimes to aws-toolki /toolkit /core /amazonq + /language-servers /language-server-runtimes ``` 2. Inside of the language-server-runtimes project run: + ``` npm install npm run compile cd runtimes npm run prepub cd out + ``` + + If you get an error running `npm run prepub`, you can instead run `npm run prepub:copyFiles` to skip cleaning and testing. + +3. Choose one of the following approaches: + +### Option A: Using npm pack (Recommended) + +3a. Create a package file: + + npm pack + +You will see a file created like this: `aws-language-server-runtimes-0.*.*.tgz` + +4a. Inside of language-servers, find the package where you need the change. + +For example, if you would like the change in `language-servers/app/aws-lsp-codewhisperer-runtimes`, you would run: + + cd language-servers/app/aws-lsp-codewhisperer-runtimes + + npm install ../../../language-server-runtimes/runtimes/out/aws-language-server-runtimes-0.*.*.tgz + + npm run compile + +5a. If you need the change in aws-toolkit-vscode run: + + cd aws-toolkit-vscode + + npm install ../language-server-runtimes/runtimes/out/aws-language-server-runtimes-0.*.*.tgz + +### Option B: Using npm link (Alternative) + +3b. Create npm links: + npm link cd ../../types npm link - ``` - If you get an error running `npm run prepub`, you can instead run `npm run prepub:copyFiles` to skip cleaning and testing. -3. Inside of aws-toolkit-vscode run: - ``` + +4b. Inside of aws-toolkit-vscode run: + npm install npm link @aws/language-server-runtimes @aws/language-server-runtimes-types - ``` ## Amazon Q Inline Activation diff --git a/package-lock.json b/package-lock.json index 737f31bb840..92aedfd4ffe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "plugins/*" ], "dependencies": { + "@aws/language-server-runtimes": "^0.3.5", "@types/node": "^22.7.5", "@types/selenium-webdriver": "^4.1.28", "buffer": "^6.0.3", @@ -24,7 +25,7 @@ "vscode-nls-dev": "^4.0.4" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.329", + "@aws-toolkits/telemetry": "^1.0.338", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", @@ -80,441 +81,467 @@ "resolved": "src.gen/@amzn/codewhisperer-streaming", "link": true }, - "node_modules/@amzn/sagemaker-client": { - "version": "1.0.0", - "resolved": "file:src.gen/@amzn/sagemaker-client/1.0.0.tgz", - "integrity": "sha512-rNMUzeACaCiIqR8aQo3G99xR+Qy6zhbGi9+6XRG5proUKetO3584dclmSnIUvDvzLWosFcl4GyP8tFqiahc6Jg==", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.363.0", - "@aws-sdk/credential-provider-node": "3.363.0", - "@aws-sdk/middleware-host-header": "3.363.0", - "@aws-sdk/middleware-logger": "3.363.0", - "@aws-sdk/middleware-recursion-detection": "3.363.0", - "@aws-sdk/middleware-signing": "3.363.0", - "@aws-sdk/middleware-user-agent": "3.363.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.363.0", - "@aws-sdk/util-user-agent-node": "3.363.0", - "@smithy/config-resolver": "^1.0.1", - "@smithy/fetch-http-handler": "^1.0.1", - "@smithy/hash-node": "^1.0.1", - "@smithy/invalid-dependency": "^1.0.1", - "@smithy/middleware-content-length": "^1.0.1", - "@smithy/middleware-retry": "^1.0.3", - "@smithy/middleware-serde": "^1.0.1", - "@smithy/middleware-stack": "^1.0.1", - "@smithy/node-config-provider": "^1.0.1", - "@smithy/node-http-handler": "^1.0.2", - "@smithy/protocol-http": "^1.1.0", - "@smithy/smithy-client": "^1.0.3", - "@smithy/types": "^1.1.0", - "@smithy/url-parser": "^1.0.1", - "@smithy/util-base64": "^1.0.1", - "@smithy/util-body-length-browser": "^1.0.1", - "@smithy/util-body-length-node": "^1.0.1", - "@smithy/util-defaults-mode-browser": "^1.0.1", - "@smithy/util-defaults-mode-node": "^1.0.1", - "@smithy/util-retry": "^1.0.3", - "@smithy/util-utf8": "^1.0.1", - "@smithy/util-waiter": "^1.0.1", - "tslib": "^2.5.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/sha256-browser": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/ie11-detection": "^3.0.0", - "@aws-crypto/sha256-js": "^3.0.0", - "@aws-crypto/supports-web-crypto": "^3.0.0", - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/sha256-js": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@amzn/glue-catalog-client": { + "version": "0.0.1", + "resolved": "file:src.gen/@amzn/glue-catalog-client/0.0.1.tgz", + "integrity": "sha512-DXE1bTaWlo32obkW0/PR0E7twmNa/3fkvBTL899lpLP4pGdzQ94Ci2io+3mb1CGC/KoyINdERC0HrA2HrqYavw==", "dependencies": { - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/sha256-js/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/supports-web-crypto": { - "version": "3.0.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-node": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.8", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/client-sso": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.922.0.tgz", + "integrity": "sha512-jdHs7uy7cSpiMvrxhYmqHyJxgK7hyqw4plG8OQ4YTBpq0SbfAxdoOuOkwJ1IVUUQho4otR1xYYjiX/8e8J8qwQ==", "license": "Apache-2.0", "dependencies": { - "tslib": "^1.11.1" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/util": { - "version": "3.0.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/core": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz", + "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" + "@aws-sdk/types": "3.922.0", + "@aws-sdk/xml-builder": "3.921.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/util/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/client-sso": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.922.0.tgz", + "integrity": "sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/middleware-host-header": "3.363.0", - "@aws-sdk/middleware-logger": "3.363.0", - "@aws-sdk/middleware-recursion-detection": "3.363.0", - "@aws-sdk/middleware-user-agent": "3.363.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.363.0", - "@aws-sdk/util-user-agent-node": "3.363.0", - "@smithy/config-resolver": "^1.0.1", - "@smithy/fetch-http-handler": "^1.0.1", - "@smithy/hash-node": "^1.0.1", - "@smithy/invalid-dependency": "^1.0.1", - "@smithy/middleware-content-length": "^1.0.1", - "@smithy/middleware-endpoint": "^1.0.1", - "@smithy/middleware-retry": "^1.0.2", - "@smithy/middleware-serde": "^1.0.1", - "@smithy/middleware-stack": "^1.0.1", - "@smithy/node-config-provider": "^1.0.1", - "@smithy/node-http-handler": "^1.0.2", - "@smithy/protocol-http": "^1.0.1", - "@smithy/smithy-client": "^1.0.3", - "@smithy/types": "^1.0.0", - "@smithy/url-parser": "^1.0.1", - "@smithy/util-base64": "^1.0.1", - "@smithy/util-body-length-browser": "^1.0.1", - "@smithy/util-body-length-node": "^1.0.1", - "@smithy/util-defaults-mode-browser": "^1.0.1", - "@smithy/util-defaults-mode-node": "^1.0.1", - "@smithy/util-retry": "^1.0.2", - "@smithy/util-utf8": "^1.0.1", - "tslib": "^2.5.0" + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.922.0.tgz", + "integrity": "sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/middleware-host-header": "3.363.0", - "@aws-sdk/middleware-logger": "3.363.0", - "@aws-sdk/middleware-recursion-detection": "3.363.0", - "@aws-sdk/middleware-user-agent": "3.363.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.363.0", - "@aws-sdk/util-user-agent-node": "3.363.0", - "@smithy/config-resolver": "^1.0.1", - "@smithy/fetch-http-handler": "^1.0.1", - "@smithy/hash-node": "^1.0.1", - "@smithy/invalid-dependency": "^1.0.1", - "@smithy/middleware-content-length": "^1.0.1", - "@smithy/middleware-endpoint": "^1.0.1", - "@smithy/middleware-retry": "^1.0.2", - "@smithy/middleware-serde": "^1.0.1", - "@smithy/middleware-stack": "^1.0.1", - "@smithy/node-config-provider": "^1.0.1", - "@smithy/node-http-handler": "^1.0.2", - "@smithy/protocol-http": "^1.0.1", - "@smithy/smithy-client": "^1.0.3", - "@smithy/types": "^1.0.0", - "@smithy/url-parser": "^1.0.1", - "@smithy/util-base64": "^1.0.1", - "@smithy/util-body-length-browser": "^1.0.1", - "@smithy/util-body-length-node": "^1.0.1", - "@smithy/util-defaults-mode-browser": "^1.0.1", - "@smithy/util-defaults-mode-node": "^1.0.1", - "@smithy/util-retry": "^1.0.2", - "@smithy/util-utf8": "^1.0.1", - "tslib": "^2.5.0" + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-stream": "^4.5.5", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/client-sts": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.922.0.tgz", + "integrity": "sha512-bVF+pI5UCLNkvbiZr/t2fgTtv84s8FCdOGAPxQiQcw5qOZywNuuCCY3wIIchmQr6GJr8YFkEp5LgDCac5EC5aQ==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/credential-provider-node": "3.363.0", - "@aws-sdk/middleware-host-header": "3.363.0", - "@aws-sdk/middleware-logger": "3.363.0", - "@aws-sdk/middleware-recursion-detection": "3.363.0", - "@aws-sdk/middleware-sdk-sts": "3.363.0", - "@aws-sdk/middleware-signing": "3.363.0", - "@aws-sdk/middleware-user-agent": "3.363.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.363.0", - "@aws-sdk/util-user-agent-node": "3.363.0", - "@smithy/config-resolver": "^1.0.1", - "@smithy/fetch-http-handler": "^1.0.1", - "@smithy/hash-node": "^1.0.1", - "@smithy/invalid-dependency": "^1.0.1", - "@smithy/middleware-content-length": "^1.0.1", - "@smithy/middleware-endpoint": "^1.0.1", - "@smithy/middleware-retry": "^1.0.1", - "@smithy/middleware-serde": "^1.0.1", - "@smithy/middleware-stack": "^1.0.1", - "@smithy/node-config-provider": "^1.0.1", - "@smithy/node-http-handler": "^1.0.1", - "@smithy/protocol-http": "^1.1.0", - "@smithy/smithy-client": "^1.0.2", - "@smithy/types": "^1.1.0", - "@smithy/url-parser": "^1.0.1", - "@smithy/util-base64": "^1.0.1", - "@smithy/util-body-length-browser": "^1.0.1", - "@smithy/util-body-length-node": "^1.0.1", - "@smithy/util-defaults-mode-browser": "^1.0.1", - "@smithy/util-defaults-mode-node": "^1.0.1", - "@smithy/util-retry": "^1.0.1", - "@smithy/util-utf8": "^1.0.1", - "fast-xml-parser": "4.2.5", - "tslib": "^2.5.0" + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.922.0", + "@aws-sdk/credential-provider-web-identity": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.922.0.tgz", + "integrity": "sha512-agCwaD6mBihToHkjycL8ObIS2XOnWypWZZWhJSoWyHwFrhEKz1zGvgylK9Dc711oUfU+zU6J8e0JPKNJMNb3BQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.357.0", - "@smithy/property-provider": "^1.0.1", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-ini": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.922.0", + "@aws-sdk/credential-provider-web-identity": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz", + "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.363.0", - "@aws-sdk/credential-provider-process": "3.363.0", - "@aws-sdk/credential-provider-sso": "3.363.0", - "@aws-sdk/credential-provider-web-identity": "3.363.0", - "@aws-sdk/types": "3.357.0", - "@smithy/credential-provider-imds": "^1.0.1", - "@smithy/property-provider": "^1.0.1", - "@smithy/shared-ini-file-loader": "^1.0.1", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.922.0.tgz", + "integrity": "sha512-nbD3G3hShTYxLCkKMqLkLPtKwAAfxdY/k9jHtZmVBFXek2T6tQrqZHKxlAu+fd23Ga4/Aik7DLQQx1RA1a5ipg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.363.0", - "@aws-sdk/credential-provider-ini": "3.363.0", - "@aws-sdk/credential-provider-process": "3.363.0", - "@aws-sdk/credential-provider-sso": "3.363.0", - "@aws-sdk/credential-provider-web-identity": "3.363.0", - "@aws-sdk/types": "3.357.0", - "@smithy/credential-provider-imds": "^1.0.1", - "@smithy/property-provider": "^1.0.1", - "@smithy/shared-ini-file-loader": "^1.0.1", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-sdk/client-sso": "3.922.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/token-providers": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.922.0.tgz", + "integrity": "sha512-wjGIhgMHGGQfQTdFaJphNOKyAL8wZs6znJdHADPVURmgR+EWLyN/0fDO1u7wx8xaLMZpbHIFWBEvf9TritR/cQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.357.0", - "@smithy/property-provider": "^1.0.1", - "@smithy/shared-ini-file-loader": "^1.0.1", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", + "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.363.0", - "@aws-sdk/token-providers": "3.363.0", - "@aws-sdk/types": "3.357.0", - "@smithy/property-provider": "^1.0.1", - "@smithy/shared-ini-file-loader": "^1.0.1", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/middleware-logger": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", + "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.357.0", - "@smithy/property-provider": "^1.0.1", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", + "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.357.0", - "@smithy/protocol-http": "^1.1.0", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-sdk/types": "3.922.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/middleware-logger": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz", + "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.357.0", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@smithy/core": "^3.17.2", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/nested-clients": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.922.0.tgz", + "integrity": "sha512-uYvKCF1TGh/MuJ4TMqmUM0Csuao02HawcseG4LUDyxdUsd/EFuxalWq1Cx4fKZQ2K8F504efZBjctMAMNY+l7A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.357.0", - "@smithy/protocol-http": "^1.1.0", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.922.0.tgz", + "integrity": "sha512-44Y/rNNwhngR2KHp6gkx//TOr56/hx6s4l+XLjOqH7EBCHL7XhnrT1y92L+DLiroVr1tCSmO8eHQwBv0Y2+mvw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@smithy/protocol-http": "^1.1.0", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-sdk/types": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/token-providers": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/token-providers": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.922.0.tgz", + "integrity": "sha512-/inmPnjZE0ZBE16zaCowAvouSx05FJ7p6BQYuzlJ8vxEU0sS0Hf8fvhuiRnN9V9eDUPIBY+/5EjbMWygXL4wlQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso-oidc": "3.363.0", - "@aws-sdk/types": "3.357.0", - "@smithy/property-provider": "^1.0.1", - "@smithy/shared-ini-file-loader": "^1.0.1", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/types": { - "version": "3.357.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/types": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.5.0" + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/util-endpoints": { - "version": "3.357.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/util-endpoints": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", + "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-endpoints": "^3.2.4", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", + "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.357.0", - "@smithy/types": "^1.1.0", + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", "bowser": "^2.11.0", - "tslib": "^2.5.0" + "tslib": "^2.6.2" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.363.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.922.0.tgz", + "integrity": "sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.357.0", - "@smithy/node-config-provider": "^1.0.1", - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" }, "peerDependencies": { "aws-crt": ">=1.0.0" @@ -525,436 +552,602 @@ } } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/abort-controller": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws-sdk/xml-builder": { + "version": "3.921.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", + "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/types": "^4.8.1", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/config-resolver": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@aws/lambda-invoke-store": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", + "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^1.2.0", - "@smithy/util-config-provider": "^1.1.0", - "@smithy/util-middleware": "^1.1.0", - "tslib": "^2.5.0" - }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/credential-provider-imds": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/abort-controller": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", + "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^1.1.0", - "@smithy/property-provider": "^1.2.0", - "@smithy/types": "^1.2.0", - "@smithy/url-parser": "^1.1.0", - "tslib": "^2.5.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/fetch-http-handler": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/config-resolver": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", + "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^1.2.0", - "@smithy/querystring-builder": "^1.1.0", - "@smithy/types": "^1.2.0", - "@smithy/util-base64": "^1.1.0", - "tslib": "^2.5.0" + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/hash-node": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/core": { + "version": "3.18.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.6.tgz", + "integrity": "sha512-8Q/ugWqfDUEU1Exw71+DoOzlONJ2Cn9QA8VeeDzLLjzO/qruh9UKFzbszy4jXcIYgGofxYiT0t1TT6+CT/GupQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^1.2.0", - "@smithy/util-buffer-from": "^1.1.0", - "@smithy/util-utf8": "^1.1.0", - "tslib": "^2.5.0" + "@smithy/middleware-serde": "^4.2.6", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/invalid-dependency": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/credential-provider-imds": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", + "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/is-array-buffer": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/fetch-http-handler": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", + "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.5.0" + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/middleware-content-length": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/hash-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", + "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^1.2.0", - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/types": "^4.9.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/middleware-endpoint": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/invalid-dependency": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", + "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^1.1.0", - "@smithy/types": "^1.2.0", - "@smithy/url-parser": "^1.1.0", - "@smithy/util-middleware": "^1.1.0", - "tslib": "^2.5.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/middleware-retry": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^1.2.0", - "@smithy/service-error-classification": "^1.1.0", - "@smithy/types": "^1.2.0", - "@smithy/util-middleware": "^1.1.0", - "@smithy/util-retry": "^1.1.0", - "tslib": "^2.5.0", - "uuid": "^8.3.2" + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/middleware-serde": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/middleware-content-length": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", + "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/middleware-stack": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/middleware-endpoint": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.13.tgz", + "integrity": "sha512-X4za1qCdyx1hEVVXuAWlZuK6wzLDv1uw1OY9VtaYy1lULl661+frY7FeuHdYdl7qAARUxH2yvNExU2/SmRFfcg==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.5.0" + "@smithy/core": "^3.18.6", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/node-config-provider": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/middleware-retry": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.13.tgz", + "integrity": "sha512-RzIDF9OrSviXX7MQeKOm8r/372KTyY8Jmp6HNKOOYlrguHADuM3ED/f4aCyNhZZFLG55lv5beBin7nL0Nzy1Dw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^1.2.0", - "@smithy/shared-ini-file-loader": "^1.1.0", - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/smithy-client": "^4.9.9", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/node-http-handler": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/middleware-serde": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", + "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^1.1.0", - "@smithy/protocol-http": "^1.2.0", - "@smithy/querystring-builder": "^1.1.0", - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/property-provider": { - "version": "1.2.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/middleware-stack": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", + "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/protocol-http": { - "version": "1.2.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/node-config-provider": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", + "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/querystring-builder": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/node-http-handler": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", + "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^1.2.0", - "@smithy/util-uri-escape": "^1.1.0", - "tslib": "^2.5.0" + "@smithy/abort-controller": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/querystring-parser": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/property-provider": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", + "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/service-error-classification": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/protocol-http": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", + "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/shared-ini-file-loader": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/querystring-builder": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", + "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/types": "^4.9.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/smithy-client": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/querystring-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", + "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-stack": "^1.1.0", - "@smithy/types": "^1.2.0", - "@smithy/util-stream": "^1.1.0", - "tslib": "^2.5.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/types": { - "version": "1.2.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/service-error-classification": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", + "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.5.0" + "@smithy/types": "^4.9.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/url-parser": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", + "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^1.1.0", - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-base64": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/signature-v4": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", + "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^1.1.0", - "tslib": "^2.5.0" + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-body-length-browser": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/smithy-client": { + "version": "4.9.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.9.tgz", + "integrity": "sha512-SUnZJMMo5yCmgjopJbiNeo1vlr8KvdnEfIHV9rlD77QuOGdRotIVBcOrBuMr+sI9zrnhtDtLP054bZVbpZpiQA==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.5.0" + "@smithy/core": "^3.18.6", + "@smithy/middleware-endpoint": "^4.3.13", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-body-length-node": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/types": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", + "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-buffer-from": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/url-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", + "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^1.1.0", - "tslib": "^2.5.0" + "@smithy/querystring-parser": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-config-provider": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.5.0" + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-defaults-mode-browser": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^1.2.0", - "@smithy/types": "^1.2.0", - "bowser": "^2.11.0", - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">= 10.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-defaults-mode-node": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^1.1.0", - "@smithy/credential-provider-imds": "^1.1.0", - "@smithy/node-config-provider": "^1.1.0", - "@smithy/property-provider": "^1.2.0", - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">= 10.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-hex-encoding": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.5.0" + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-middleware": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-retry": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.12.tgz", + "integrity": "sha512-TKc6FnOxFULKxLgTNHYjcFqdOYzXVPFFVm5JhI30F3RdhT7nYOtOsjgaOwfDRmA/3U66O9KaBQ3UHoXwayRhAg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^1.1.0", - "tslib": "^2.5.0" + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.9", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">= 14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-stream": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.15.tgz", + "integrity": "sha512-94NqfQVo+vGc5gsQ9SROZqOvBkGNMQu6pjXbnn8aQvBUhc31kx49gxlkBEqgmaZQHUUfdRUin5gK/HlHKmbAwg==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^1.1.0", - "@smithy/node-http-handler": "^1.1.0", - "@smithy/types": "^1.2.0", - "@smithy/util-base64": "^1.1.0", - "@smithy/util-buffer-from": "^1.1.0", - "@smithy/util-hex-encoding": "^1.1.0", - "@smithy/util-utf8": "^1.1.0", - "tslib": "^2.5.0" + "@smithy/config-resolver": "^4.4.3", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.9", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-uri-escape": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-endpoints": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", + "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.5.0" + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-utf8": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^1.1.0", - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-waiter": { - "version": "1.1.0", + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-middleware": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", + "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^1.1.0", - "@smithy/types": "^1.2.0", - "tslib": "^2.5.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@amzn/sagemaker-client/node_modules/fast-xml-parser": { + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-retry": { "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", + "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-stream": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", + "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@amzn/glue-catalog-client/node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@amzn/glue-catalog-client/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", "funding": [ - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - }, { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" @@ -962,586 +1155,9256 @@ ], "license": "MIT", "dependencies": { - "strnum": "^1.0.5" + "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, - "node_modules/@amzn/sagemaker-client/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "node_modules/@amzn/glue-catalog-client/node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@amzn/sagemaker-client": { + "version": "1.0.0", + "resolved": "file:src.gen/@amzn/sagemaker-client/1.0.0.tgz", + "integrity": "sha512-rNMUzeACaCiIqR8aQo3G99xR+Qy6zhbGi9+6XRG5proUKetO3584dclmSnIUvDvzLWosFcl4GyP8tFqiahc6Jg==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.363.0", + "@aws-sdk/credential-provider-node": "3.363.0", + "@aws-sdk/middleware-host-header": "3.363.0", + "@aws-sdk/middleware-logger": "3.363.0", + "@aws-sdk/middleware-recursion-detection": "3.363.0", + "@aws-sdk/middleware-signing": "3.363.0", + "@aws-sdk/middleware-user-agent": "3.363.0", + "@aws-sdk/types": "3.357.0", + "@aws-sdk/util-user-agent-browser": "3.363.0", + "@aws-sdk/util-user-agent-node": "3.363.0", + "@smithy/config-resolver": "^1.0.1", + "@smithy/fetch-http-handler": "^1.0.1", + "@smithy/hash-node": "^1.0.1", + "@smithy/invalid-dependency": "^1.0.1", + "@smithy/middleware-content-length": "^1.0.1", + "@smithy/middleware-retry": "^1.0.3", + "@smithy/middleware-serde": "^1.0.1", + "@smithy/middleware-stack": "^1.0.1", + "@smithy/node-config-provider": "^1.0.1", + "@smithy/node-http-handler": "^1.0.2", + "@smithy/protocol-http": "^1.1.0", + "@smithy/smithy-client": "^1.0.3", + "@smithy/types": "^1.1.0", + "@smithy/url-parser": "^1.0.1", + "@smithy/util-base64": "^1.0.1", + "@smithy/util-body-length-browser": "^1.0.1", + "@smithy/util-body-length-node": "^1.0.1", + "@smithy/util-defaults-mode-browser": "^1.0.1", + "@smithy/util-defaults-mode-node": "^1.0.1", + "@smithy/util-retry": "^1.0.3", + "@smithy/util-utf8": "^1.0.1", + "@smithy/util-waiter": "^1.0.1", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@aws-crypto/crc32": { - "version": "5.2.0", + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/sha256-browser": { + "version": "3.0.0", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/util": "^5.2.0", + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" } }, - "node_modules/@aws-crypto/crc32c": { - "version": "5.2.0", + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/sha256-js": { + "version": "3.0.0", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/util": "^5.2.0", + "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" + "tslib": "^1.11.1" } }, - "node_modules/@aws-crypto/ie11-detection": { + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/supports-web-crypto": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { "tslib": "^1.11.1" } }, - "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { "version": "1.14.1", "license": "0BSD" }, - "node_modules/@aws-crypto/sha1-browser": { - "version": "5.2.0", + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/util": { + "version": "3.0.0", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" } }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/client-sso": { + "version": "3.363.0", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/middleware-host-header": "3.363.0", + "@aws-sdk/middleware-logger": "3.363.0", + "@aws-sdk/middleware-recursion-detection": "3.363.0", + "@aws-sdk/middleware-user-agent": "3.363.0", + "@aws-sdk/types": "3.357.0", + "@aws-sdk/util-endpoints": "3.357.0", + "@aws-sdk/util-user-agent-browser": "3.363.0", + "@aws-sdk/util-user-agent-node": "3.363.0", + "@smithy/config-resolver": "^1.0.1", + "@smithy/fetch-http-handler": "^1.0.1", + "@smithy/hash-node": "^1.0.1", + "@smithy/invalid-dependency": "^1.0.1", + "@smithy/middleware-content-length": "^1.0.1", + "@smithy/middleware-endpoint": "^1.0.1", + "@smithy/middleware-retry": "^1.0.2", + "@smithy/middleware-serde": "^1.0.1", + "@smithy/middleware-stack": "^1.0.1", + "@smithy/node-config-provider": "^1.0.1", + "@smithy/node-http-handler": "^1.0.2", + "@smithy/protocol-http": "^1.0.1", + "@smithy/smithy-client": "^1.0.3", + "@smithy/types": "^1.0.0", + "@smithy/url-parser": "^1.0.1", + "@smithy/util-base64": "^1.0.1", + "@smithy/util-body-length-browser": "^1.0.1", + "@smithy/util-body-length-node": "^1.0.1", + "@smithy/util-defaults-mode-browser": "^1.0.1", + "@smithy/util-defaults-mode-node": "^1.0.1", + "@smithy/util-retry": "^1.0.2", + "@smithy/util-utf8": "^1.0.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.363.0", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/middleware-host-header": "3.363.0", + "@aws-sdk/middleware-logger": "3.363.0", + "@aws-sdk/middleware-recursion-detection": "3.363.0", + "@aws-sdk/middleware-user-agent": "3.363.0", + "@aws-sdk/types": "3.357.0", + "@aws-sdk/util-endpoints": "3.357.0", + "@aws-sdk/util-user-agent-browser": "3.363.0", + "@aws-sdk/util-user-agent-node": "3.363.0", + "@smithy/config-resolver": "^1.0.1", + "@smithy/fetch-http-handler": "^1.0.1", + "@smithy/hash-node": "^1.0.1", + "@smithy/invalid-dependency": "^1.0.1", + "@smithy/middleware-content-length": "^1.0.1", + "@smithy/middleware-endpoint": "^1.0.1", + "@smithy/middleware-retry": "^1.0.2", + "@smithy/middleware-serde": "^1.0.1", + "@smithy/middleware-stack": "^1.0.1", + "@smithy/node-config-provider": "^1.0.1", + "@smithy/node-http-handler": "^1.0.2", + "@smithy/protocol-http": "^1.0.1", + "@smithy/smithy-client": "^1.0.3", + "@smithy/types": "^1.0.0", + "@smithy/url-parser": "^1.0.1", + "@smithy/util-base64": "^1.0.1", + "@smithy/util-body-length-browser": "^1.0.1", + "@smithy/util-body-length-node": "^1.0.1", + "@smithy/util-defaults-mode-browser": "^1.0.1", + "@smithy/util-defaults-mode-node": "^1.0.1", + "@smithy/util-retry": "^1.0.2", + "@smithy/util-utf8": "^1.0.1", + "tslib": "^2.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/client-sts": { + "version": "3.363.0", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.6.2" + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/credential-provider-node": "3.363.0", + "@aws-sdk/middleware-host-header": "3.363.0", + "@aws-sdk/middleware-logger": "3.363.0", + "@aws-sdk/middleware-recursion-detection": "3.363.0", + "@aws-sdk/middleware-sdk-sts": "3.363.0", + "@aws-sdk/middleware-signing": "3.363.0", + "@aws-sdk/middleware-user-agent": "3.363.0", + "@aws-sdk/types": "3.357.0", + "@aws-sdk/util-endpoints": "3.357.0", + "@aws-sdk/util-user-agent-browser": "3.363.0", + "@aws-sdk/util-user-agent-node": "3.363.0", + "@smithy/config-resolver": "^1.0.1", + "@smithy/fetch-http-handler": "^1.0.1", + "@smithy/hash-node": "^1.0.1", + "@smithy/invalid-dependency": "^1.0.1", + "@smithy/middleware-content-length": "^1.0.1", + "@smithy/middleware-endpoint": "^1.0.1", + "@smithy/middleware-retry": "^1.0.1", + "@smithy/middleware-serde": "^1.0.1", + "@smithy/middleware-stack": "^1.0.1", + "@smithy/node-config-provider": "^1.0.1", + "@smithy/node-http-handler": "^1.0.1", + "@smithy/protocol-http": "^1.1.0", + "@smithy/smithy-client": "^1.0.2", + "@smithy/types": "^1.1.0", + "@smithy/url-parser": "^1.0.1", + "@smithy/util-base64": "^1.0.1", + "@smithy/util-body-length-browser": "^1.0.1", + "@smithy/util-body-length-node": "^1.0.1", + "@smithy/util-defaults-mode-browser": "^1.0.1", + "@smithy/util-defaults-mode-node": "^1.0.1", + "@smithy/util-retry": "^1.0.1", + "@smithy/util-utf8": "^1.0.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.363.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" + "@aws-sdk/types": "3.357.0", + "@smithy/property-provider": "^1.0.1", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway": { - "version": "3.693.0", + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.363.0", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.693.0", - "@aws-sdk/client-sts": "3.693.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-sdk-api-gateway": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", + "@aws-sdk/credential-provider-env": "3.363.0", + "@aws-sdk/credential-provider-process": "3.363.0", + "@aws-sdk/credential-provider-sso": "3.363.0", + "@aws-sdk/credential-provider-web-identity": "3.363.0", + "@aws-sdk/types": "3.357.0", + "@smithy/credential-provider-imds": "^1.0.1", + "@smithy/property-provider": "^1.0.1", + "@smithy/shared-ini-file-loader": "^1.0.1", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.363.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.363.0", + "@aws-sdk/credential-provider-ini": "3.363.0", + "@aws-sdk/credential-provider-process": "3.363.0", + "@aws-sdk/credential-provider-sso": "3.363.0", + "@aws-sdk/credential-provider-web-identity": "3.363.0", + "@aws-sdk/types": "3.357.0", + "@smithy/credential-provider-imds": "^1.0.1", + "@smithy/property-provider": "^1.0.1", + "@smithy/shared-ini-file-loader": "^1.0.1", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.363.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.357.0", + "@smithy/property-provider": "^1.0.1", + "@smithy/shared-ini-file-loader": "^1.0.1", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.363.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.363.0", + "@aws-sdk/token-providers": "3.363.0", + "@aws-sdk/types": "3.357.0", + "@smithy/property-provider": "^1.0.1", + "@smithy/shared-ini-file-loader": "^1.0.1", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.363.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.357.0", + "@smithy/property-provider": "^1.0.1", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.363.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.357.0", + "@smithy/protocol-http": "^1.1.0", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/middleware-logger": { + "version": "3.363.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.357.0", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.363.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.357.0", + "@smithy/protocol-http": "^1.1.0", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.363.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.357.0", + "@aws-sdk/util-endpoints": "3.357.0", + "@smithy/protocol-http": "^1.1.0", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/token-providers": { + "version": "3.363.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso-oidc": "3.363.0", + "@aws-sdk/types": "3.357.0", + "@smithy/property-provider": "^1.0.1", + "@smithy/shared-ini-file-loader": "^1.0.1", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/types": { + "version": "3.357.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/util-endpoints": { + "version": "3.357.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.357.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.363.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.357.0", + "@smithy/types": "^1.1.0", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.363.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.357.0", + "@smithy/node-config-provider": "^1.0.1", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/abort-controller": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/config-resolver": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^1.2.0", + "@smithy/util-config-provider": "^1.1.0", + "@smithy/util-middleware": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/credential-provider-imds": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^1.1.0", + "@smithy/property-provider": "^1.2.0", + "@smithy/types": "^1.2.0", + "@smithy/url-parser": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/fetch-http-handler": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^1.2.0", + "@smithy/querystring-builder": "^1.1.0", + "@smithy/types": "^1.2.0", + "@smithy/util-base64": "^1.1.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/hash-node": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^1.2.0", + "@smithy/util-buffer-from": "^1.1.0", + "@smithy/util-utf8": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/invalid-dependency": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/is-array-buffer": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/middleware-content-length": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^1.2.0", + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/middleware-endpoint": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^1.1.0", + "@smithy/types": "^1.2.0", + "@smithy/url-parser": "^1.1.0", + "@smithy/util-middleware": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/middleware-retry": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^1.2.0", + "@smithy/service-error-classification": "^1.1.0", + "@smithy/types": "^1.2.0", + "@smithy/util-middleware": "^1.1.0", + "@smithy/util-retry": "^1.1.0", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/middleware-serde": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/middleware-stack": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/node-config-provider": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^1.2.0", + "@smithy/shared-ini-file-loader": "^1.1.0", + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/node-http-handler": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^1.1.0", + "@smithy/protocol-http": "^1.2.0", + "@smithy/querystring-builder": "^1.1.0", + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/property-provider": { + "version": "1.2.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/protocol-http": { + "version": "1.2.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/querystring-builder": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^1.2.0", + "@smithy/util-uri-escape": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/querystring-parser": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/service-error-classification": { + "version": "1.1.0", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/shared-ini-file-loader": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/smithy-client": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-stack": "^1.1.0", + "@smithy/types": "^1.2.0", + "@smithy/util-stream": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/types": { + "version": "1.2.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/url-parser": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^1.1.0", + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-base64": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-body-length-browser": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-body-length-node": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-buffer-from": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-config-provider": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-defaults-mode-browser": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^1.2.0", + "@smithy/types": "^1.2.0", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-defaults-mode-node": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^1.1.0", + "@smithy/credential-provider-imds": "^1.1.0", + "@smithy/node-config-provider": "^1.1.0", + "@smithy/property-provider": "^1.2.0", + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-hex-encoding": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-middleware": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-retry": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-stream": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^1.1.0", + "@smithy/node-http-handler": "^1.1.0", + "@smithy/types": "^1.2.0", + "@smithy/util-base64": "^1.1.0", + "@smithy/util-buffer-from": "^1.1.0", + "@smithy/util-hex-encoding": "^1.1.0", + "@smithy/util-utf8": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-uri-escape": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-utf8": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-waiter": { + "version": "1.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^1.1.0", + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/fast-xml-parser": { + "version": "4.2.5", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/uuid": { + "version": "8.3.2", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/ie11-detection": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-accessanalyzer/-/client-accessanalyzer-3.888.0.tgz", + "integrity": "sha512-wtyBy3z2sUvuJxEcQhere+ttQWIVx5GauJaYahWAWBRhuZIkqMMebKC0ofJMBSEGTRXL98L3G96pCwoIffFbBw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/credential-provider-node": "3.888.0", + "@aws-sdk/middleware-host-header": "3.887.0", + "@aws-sdk/middleware-logger": "3.887.0", + "@aws-sdk/middleware-recursion-detection": "3.887.0", + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-user-agent-browser": "3.887.0", + "@aws-sdk/util-user-agent-node": "3.888.0", + "@smithy/config-resolver": "^4.2.1", + "@smithy/core": "^3.11.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.1", + "@smithy/util-defaults-mode-node": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/client-sso": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.888.0.tgz", + "integrity": "sha512-8CLy/ehGKUmekjH+VtZJ4w40PqDg3u0K7uPziq/4P8Q7LLgsy8YQoHNbuY4am7JU3HWrqLXJI9aaz1+vPGPoWA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/middleware-host-header": "3.887.0", + "@aws-sdk/middleware-logger": "3.887.0", + "@aws-sdk/middleware-recursion-detection": "3.887.0", + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-user-agent-browser": "3.887.0", + "@aws-sdk/util-user-agent-node": "3.888.0", + "@smithy/config-resolver": "^4.2.1", + "@smithy/core": "^3.11.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.1", + "@smithy/util-defaults-mode-node": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/core": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.888.0.tgz", + "integrity": "sha512-L3S2FZywACo4lmWv37Y4TbefuPJ1fXWyWwIJ3J4wkPYFJ47mmtUPqThlVrSbdTHkEjnZgJe5cRfxk0qCLsFh1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@aws-sdk/xml-builder": "3.887.0", + "@smithy/core": "^3.11.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.888.0.tgz", + "integrity": "sha512-shPi4AhUKbIk7LugJWvNpeZA8va7e5bOHAEKo89S0Ac8WDZt2OaNzbh/b9l0iSL2eEyte8UgIsYGcFxOwIF1VA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.888.0.tgz", + "integrity": "sha512-Jvuk6nul0lE7o5qlQutcqlySBHLXOyoPtiwE6zyKbGc7RVl0//h39Lab7zMeY2drMn8xAnIopL4606Fd8JI/Hw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.888.0.tgz", + "integrity": "sha512-M82ItvS5yq+tO6ZOV1ruaVs2xOne+v8HW85GFCXnz8pecrzYdgxh6IsVqEbbWruryG/mUGkWMbkBZoEsy4MgyA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/credential-provider-env": "3.888.0", + "@aws-sdk/credential-provider-http": "3.888.0", + "@aws-sdk/credential-provider-process": "3.888.0", + "@aws-sdk/credential-provider-sso": "3.888.0", + "@aws-sdk/credential-provider-web-identity": "3.888.0", + "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.888.0.tgz", + "integrity": "sha512-KCrQh1dCDC8Y+Ap3SZa6S81kHk+p+yAaOQ5jC3dak4zhHW3RCrsGR/jYdemTOgbEGcA6ye51UbhWfrrlMmeJSA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.888.0", + "@aws-sdk/credential-provider-http": "3.888.0", + "@aws-sdk/credential-provider-ini": "3.888.0", + "@aws-sdk/credential-provider-process": "3.888.0", + "@aws-sdk/credential-provider-sso": "3.888.0", + "@aws-sdk/credential-provider-web-identity": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.888.0.tgz", + "integrity": "sha512-+aX6piSukPQ8DUS4JAH344GePg8/+Q1t0+kvSHAZHhYvtQ/1Zek3ySOJWH2TuzTPCafY4nmWLcQcqvU1w9+4Lw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.888.0.tgz", + "integrity": "sha512-b1ZJji7LJ6E/j1PhFTyvp51in2iCOQ3VP6mj5H6f5OUnqn7efm41iNMoinKr87n0IKZw7qput5ggXVxEdPhouA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.888.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/token-providers": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.888.0.tgz", + "integrity": "sha512-7P0QNtsDzMZdmBAaY/vY1BsZHwTGvEz3bsn2bm5VSKFAeMmZqsHK1QeYdNsFjLtegnVh+wodxMq50jqLv3LFlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.887.0.tgz", + "integrity": "sha512-ulzqXv6NNqdu/kr0sgBYupWmahISHY+azpJidtK6ZwQIC+vBUk9NdZeqQpy7KVhIk2xd4+5Oq9rxapPwPI21CA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/middleware-logger": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.887.0.tgz", + "integrity": "sha512-YbbgLI6jKp2qSoAcHnXrQ5jcuc5EYAmGLVFgMVdk8dfCfJLfGGSaOLxF4CXC7QYhO50s+mPPkhBYejCik02Kug==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.887.0.tgz", + "integrity": "sha512-tjrUXFtQnFLo+qwMveq5faxP5MQakoLArXtqieHphSqZTXm21wDJM73hgT4/PQQGTwgYjDKqnqsE1hvk0hcfDw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.888.0.tgz", + "integrity": "sha512-ZkcUkoys8AdrNNG7ATjqw2WiXqrhTvT+r4CIK3KhOqIGPHX0p0DQWzqjaIl7ZhSUToKoZ4Ud7MjF795yUr73oA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@smithy/core": "^3.11.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/nested-clients": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.888.0.tgz", + "integrity": "sha512-py4o4RPSGt+uwGvSBzR6S6cCBjS4oTX5F8hrHFHfPCdIOMVjyOBejn820jXkCrcdpSj3Qg1yUZXxsByvxc9Lyg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/middleware-host-header": "3.887.0", + "@aws-sdk/middleware-logger": "3.887.0", + "@aws-sdk/middleware-recursion-detection": "3.887.0", + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-user-agent-browser": "3.887.0", + "@aws-sdk/util-user-agent-node": "3.888.0", + "@smithy/config-resolver": "^4.2.1", + "@smithy/core": "^3.11.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.1", + "@smithy/util-defaults-mode-node": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.887.0.tgz", + "integrity": "sha512-VdSMrIqJ3yjJb/fY+YAxrH/lCVv0iL8uA+lbMNfQGtO5tB3Zx6SU9LEpUwBNX8fPK1tUpI65CNE4w42+MY/7Mg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/token-providers": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.888.0.tgz", + "integrity": "sha512-WA3NF+3W8GEuCMG1WvkDYbB4z10G3O8xuhT7QSjhvLYWQ9CPt3w4VpVIfdqmUn131TCIbhCzD0KN/1VJTjAjyw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/types": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.887.0.tgz", + "integrity": "sha512-fmTEJpUhsPsovQ12vZSpVTEP/IaRoJAMBGQXlQNjtCpkBp6Iq3KQDa/HDaPINE+3xxo6XvTdtibsNOd5zJLV9A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/util-endpoints": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.887.0.tgz", + "integrity": "sha512-kpegvT53KT33BMeIcGLPA65CQVxLUL/C3gTz9AzlU/SDmeusBHX4nRApAicNzI/ltQ5lxZXbQn18UczzBuwF1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.887.0.tgz", + "integrity": "sha512-X71UmVsYc6ZTH4KU6hA5urOzYowSXc3qvroagJNLJYU1ilgZ529lP4J9XOYfEvTXkLR1hPFSRxa43SrwgelMjA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/types": "^4.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.888.0.tgz", + "integrity": "sha512-rSB3OHyuKXotIGfYEo//9sU0lXAUrTY28SUUnxzOGYuQsAt0XR5iYwBAp+RjV6x8f+Hmtbg0PdCsy1iNAXa0UQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/types": "^4.5.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-accessanalyzer/node_modules/@aws-sdk/xml-builder": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.887.0.tgz", + "integrity": "sha512-lMwgWK1kNgUhHGfBvO/5uLe7TKhycwOn3eRCqsKPT9aPCx/HWuTlpcQp8oW2pCRGLS7qzcxqpQulcD+bbUL7XQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/abort-controller": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz", + "integrity": "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/config-resolver": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.2.tgz", + "integrity": "sha512-IT6MatgBWagLybZl1xQcURXRICvqz1z3APSCAI9IqdvfCkrA7RaQIEfgC6G/KvfxnDfQUDqFV+ZlixcuFznGBQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/core": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.11.0.tgz", + "integrity": "sha512-Abs5rdP1o8/OINtE49wwNeWuynCu0kme1r4RI3VXVrHr4odVDG7h7mTnw1WXXfN5Il+c25QOnrdL2y56USfxkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.1", + "@smithy/util-utf8": "^4.1.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/credential-provider-imds": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.2.tgz", + "integrity": "sha512-JlYNq8TShnqCLg0h+afqe2wLAwZpuoSgOyzhYvTgbiKBWRov+uUve+vrZEQO6lkdLOWPh7gK5dtb9dS+KGendg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/fetch-http-handler": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.2.1.tgz", + "integrity": "sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/hash-node": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.1.1.tgz", + "integrity": "sha512-H9DIU9WBLhYrvPs9v4sYvnZ1PiAI0oc8CgNQUJ1rpN3pP7QADbTOUjchI2FB764Ub0DstH5xbTqcMJu1pnVqxA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/invalid-dependency": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.1.1.tgz", + "integrity": "sha512-1AqLyFlfrrDkyES8uhINRlJXmHA2FkG+3DY8X+rmLSqmFwk3DJnvhyGzyByPyewh2jbmV+TYQBEfngQax8IFGg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/is-array-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.1.0.tgz", + "integrity": "sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/middleware-content-length": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.1.1.tgz", + "integrity": "sha512-9wlfBBgTsRvC2JxLJxv4xDGNBrZuio3AgSl0lSFX7fneW2cGskXTYpFxCdRYD2+5yzmsiTuaAJD1Wp7gWt9y9w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/middleware-endpoint": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.2.tgz", + "integrity": "sha512-M51KcwD+UeSOFtpALGf5OijWt915aQT5eJhqnMKJt7ZTfDfNcvg2UZgIgTZUoiORawb6o5lk4n3rv7vnzQXgsA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.11.0", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/middleware-retry": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.2.2.tgz", + "integrity": "sha512-KZJueEOO+PWqflv2oGx9jICpHdBYXwCI19j7e2V3IMwKgFcXc9D9q/dsTf4B+uCnYxjNoS1jpyv6pGNGRsKOXA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/protocol-http": "^5.2.1", + "@smithy/service-error-classification": "^4.1.1", + "@smithy/smithy-client": "^4.6.2", + "@smithy/types": "^4.5.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/middleware-serde": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.1.1.tgz", + "integrity": "sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/middleware-stack": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.1.1.tgz", + "integrity": "sha512-ygRnniqNcDhHzs6QAPIdia26M7e7z9gpkIMUe/pK0RsrQ7i5MblwxY8078/QCnGq6AmlUUWgljK2HlelsKIb/A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/node-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.2.tgz", + "integrity": "sha512-SYGTKyPvyCfEzIN5rD8q/bYaOPZprYUPD2f5g9M7OjaYupWOoQFYJ5ho+0wvxIRf471i2SR4GoiZ2r94Jq9h6A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/node-http-handler": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.2.1.tgz", + "integrity": "sha512-REyybygHlxo3TJICPF89N2pMQSf+p+tBJqpVe1+77Cfi9HBPReNjTgtZ1Vg73exq24vkqJskKDpfF74reXjxfw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/property-provider": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.1.1.tgz", + "integrity": "sha512-gm3ZS7DHxUbzC2wr8MUCsAabyiXY0gaj3ROWnhSx/9sPMc6eYLMM4rX81w1zsMaObj2Lq3PZtNCC1J6lpEY7zg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/protocol-http": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.2.1.tgz", + "integrity": "sha512-T8SlkLYCwfT/6m33SIU/JOVGNwoelkrvGjFKDSDtVvAXj/9gOT78JVJEas5a+ETjOu4SVvpCstKgd0PxSu/aHw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/querystring-builder": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.1.1.tgz", + "integrity": "sha512-J9b55bfimP4z/Jg1gNo+AT84hr90p716/nvxDkPGCD4W70MPms0h8KF50RDRgBGZeL83/u59DWNqJv6tEP/DHA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-uri-escape": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/querystring-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.1.1.tgz", + "integrity": "sha512-63TEp92YFz0oQ7Pj9IuI3IgnprP92LrZtRAkE3c6wLWJxfy/yOPRt39IOKerVr0JS770olzl0kGafXlAXZ1vng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/service-error-classification": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.1.tgz", + "integrity": "sha512-Iam75b/JNXyDE41UvrlM6n8DNOa/r1ylFyvgruTUx7h2Uk7vDNV9AAwP1vfL1fOL8ls0xArwEGVcGZVd7IO/Cw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.2.0.tgz", + "integrity": "sha512-OQTfmIEp2LLuWdxa8nEEPhZmiOREO6bcB6pjs0AySf4yiZhl6kMOfqmcwcY8BaBPX+0Tb+tG7/Ia/6mwpoZ7Pw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/signature-v4": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.2.1.tgz", + "integrity": "sha512-M9rZhWQLjlQVCCR37cSjHfhriGRN+FQ8UfgrYNufv66TJgk+acaggShl3KS5U/ssxivvZLlnj7QH2CUOKlxPyA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.1.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-uri-escape": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/smithy-client": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.2.tgz", + "integrity": "sha512-u82cjh/x7MlMat76Z38TRmEcG6JtrrxN4N2CSNG5o2v2S3hfLAxRgSgFqf0FKM3dglH41Evknt/HOX+7nfzZ3g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.11.0", + "@smithy/middleware-endpoint": "^4.2.2", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.5.0.tgz", + "integrity": "sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/url-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.1.1.tgz", + "integrity": "sha512-bx32FUpkhcaKlEoOMbScvc93isaSiRM75pQ5IgIBaMkT7qMlIibpPRONyx/0CvrXHzJLpOn/u6YiDX2hcvs7Dg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-base64": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.1.0.tgz", + "integrity": "sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-body-length-browser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.1.0.tgz", + "integrity": "sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-body-length-node": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.1.0.tgz", + "integrity": "sha512-BOI5dYjheZdgR9XiEM3HJcEMCXSoqbzu7CzIgYrx0UtmvtC3tC2iDGpJLsSRFffUpy8ymsg2ARMP5fR8mtuUQQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-buffer-from": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.1.0.tgz", + "integrity": "sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-config-provider": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.1.0.tgz", + "integrity": "sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.2.tgz", + "integrity": "sha512-QKrOw01DvNHKgY+3p4r9Ut4u6EHLVZ01u6SkOMe6V6v5C+nRPXJeWh72qCT1HgwU3O7sxAIu23nNh+FOpYVZKA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.2", + "@smithy/types": "^4.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.2.tgz", + "integrity": "sha512-l2yRmSfx5haYHswPxMmCR6jGwgPs5LjHLuBwlj9U7nNBMS43YV/eevj+Xq1869UYdiynnMrCKtoOYQcwtb6lKg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.2.2", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.2", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-endpoints": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.1.2.tgz", + "integrity": "sha512-+AJsaaEGb5ySvf1SKMRrPZdYHRYSzMkCoK16jWnIMpREAnflVspMIDeCVSZJuj+5muZfgGpNpijE3mUNtjv01Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-hex-encoding": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.1.0.tgz", + "integrity": "sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-middleware": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.1.1.tgz", + "integrity": "sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-retry": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.1.tgz", + "integrity": "sha512-jGeybqEZ/LIordPLMh5bnmnoIgsqnp4IEimmUp5c5voZ8yx+5kAlN5+juyr7p+f7AtZTgvhmInQk4Q0UVbrZ0Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-stream": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.1.tgz", + "integrity": "sha512-khKkW/Jqkgh6caxMWbMuox9+YfGlsk9OnHOYCGVEdYQb/XVzcORXHLYUubHmmda0pubEDncofUrPNniS9d+uAA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-uri-escape": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.1.0.tgz", + "integrity": "sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/@smithy/util-utf8": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.1.0.tgz", + "integrity": "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/client-accessanalyzer/node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@aws-sdk/client-api-gateway": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-sdk-api-gateway": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-stream": "^3.3.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/client-sso": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-api-gateway/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/client-sso": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-apprunner/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.8", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/client-sso": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.682.0", + "@aws-sdk/client-sts": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.6", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sso": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.682.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.682.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sts": { + "version": "3.682.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/core": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-middleware": "^3.0.7", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-stream": "^3.1.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.682.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-ini": "3.682.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/token-providers": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.679.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-logger": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/token-providers": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.679.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/types": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-endpoints": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "@smithy/util-endpoints": "^2.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.9", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.682.0", + "@aws-sdk/client-sts": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/eventstream-serde-browser": "^3.0.10", + "@smithy/eventstream-serde-config-resolver": "^3.0.7", + "@smithy/eventstream-serde-node": "^3.0.9", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/client-sso": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.682.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.682.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/client-sts": { + "version": "3.682.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/core": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-middleware": "^3.0.7", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-stream": "^3.1.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.682.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-ini": "3.682.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/token-providers": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.679.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-logger": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/token-providers": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.679.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/types": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/util-endpoints": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "@smithy/util-endpoints": "^2.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.679.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.682.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.9", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", "@smithy/util-defaults-mode-browser": "^3.0.26", "@smithy/util-defaults-mode-node": "^3.0.26", "@smithy/util-endpoints": "^2.1.5", "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-stream": "^3.3.0", - "@smithy/util-utf8": "^3.0.0", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@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.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-node": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@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.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/core": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.5", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-logger": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@smithy/core": "^3.1.5", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/types": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-endpoints": { + "version": "3.743.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "@smithy/util-endpoints": "^3.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/config-resolver": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/core": { + "version": "3.1.5", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.1.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/hash-node": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/invalid-dependency": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-content-length": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-endpoint": { + "version": "4.0.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-retry": { + "version": "4.0.7", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/service-error-classification": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-serde": { + "version": "4.0.2", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-stack": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/node-http-handler": { + "version": "4.0.3", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/property-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/smithy-client": { + "version": "4.1.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/url-parser": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-base64": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.7", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.7", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.0.1", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-endpoints": { + "version": "3.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-retry": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/core": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.5", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/middleware-logger": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@smithy/core": "^3.1.5", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { + "version": "3.743.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "@smithy/util-endpoints": "^3.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.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-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/config-resolver": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/core": { + "version": "3.1.5", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.1.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/hash-node": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/invalid-dependency": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-content-length": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-endpoint": { + "version": "4.0.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-retry": { + "version": "4.0.7", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/service-error-classification": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-serde": { + "version": "4.0.2", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-stack": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/node-http-handler": { + "version": "4.0.3", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/property-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/smithy-client": { + "version": "4.1.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/url-parser": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-base64": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.7", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.7", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.0.1", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-endpoints": { + "version": "3.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-retry": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@aws-sdk/core": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.5", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@aws-sdk/types": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/core": { + "version": "3.1.5", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.1.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/middleware-endpoint": { + "version": "4.0.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/middleware-serde": { + "version": "4.0.2", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/middleware-stack": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/property-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/smithy-client": { + "version": "4.1.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/url-parser": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@aws-sdk/core": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.5", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@aws-sdk/types": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/core": { + "version": "3.1.5", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.1.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/middleware-endpoint": { + "version": "4.0.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/middleware-serde": { + "version": "4.0.2", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/middleware-stack": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.0.3", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/property-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/smithy-client": { + "version": "4.1.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/url-parser": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/util-base64": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-ini": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.758.0", + "@aws-sdk/credential-provider-web-identity": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/types": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-node/node_modules/@smithy/property-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-node/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-node/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@aws-sdk/core": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.5", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@aws-sdk/types": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/core": { + "version": "3.1.5", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.1.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/middleware-endpoint": { + "version": "4.0.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/middleware-serde": { + "version": "4.0.2", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/middleware-stack": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/property-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/smithy-client": { + "version": "4.1.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/url-parser": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.758.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/token-providers": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/core": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.5", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/types": { + "version": "3.734.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/core": { + "version": "3.1.5", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.1.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/middleware-endpoint": { + "version": "4.0.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/middleware-serde": { + "version": "4.0.2", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/middleware-stack": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/property-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/smithy-client": { + "version": "4.1.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/url-parser": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/abort-controller": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/abort-controller/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds/node_modules/@smithy/property-provider": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds/node_modules/@smithy/url-parser": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/querystring-builder": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/querystring-builder/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/querystring-parser": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/querystring-parser/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/service-error-classification": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/service-error-classification/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream": { + "version": "4.1.2", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/node-http-handler": { + "version": "4.0.3", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/types": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/util-base64": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-utf8/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.730.0", + "@aws-sdk/credential-provider-node": "3.730.0", + "@aws-sdk/middleware-host-header": "3.723.0", + "@aws-sdk/middleware-logger": "3.723.0", + "@aws-sdk/middleware-recursion-detection": "3.723.0", + "@aws-sdk/middleware-user-agent": "3.730.0", + "@aws-sdk/region-config-resolver": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.730.0", + "@aws-sdk/util-user-agent-browser": "3.723.0", + "@aws-sdk/util-user-agent-node": "3.730.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-cognito-identity/node_modules/@aws-sdk/client-sso": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.730.0", + "@aws-sdk/middleware-host-header": "3.723.0", + "@aws-sdk/middleware-logger": "3.723.0", + "@aws-sdk/middleware-recursion-detection": "3.723.0", + "@aws-sdk/middleware-user-agent": "3.730.0", + "@aws-sdk/region-config-resolver": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.730.0", + "@aws-sdk/util-user-agent-browser": "3.723.0", + "@aws-sdk/util-user-agent-node": "3.730.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-cognito-identity/node_modules/@aws-sdk/core": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/core": "^3.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/signature-v4": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/util-middleware": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/util-stream": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.730.0", + "@aws-sdk/credential-provider-env": "3.730.0", + "@aws-sdk/credential-provider-http": "3.730.0", + "@aws-sdk/credential-provider-process": "3.730.0", + "@aws-sdk/credential-provider-sso": "3.730.0", + "@aws-sdk/credential-provider-web-identity": "3.730.0", + "@aws-sdk/nested-clients": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@smithy/credential-provider-imds": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.730.0", + "@aws-sdk/credential-provider-http": "3.730.0", + "@aws-sdk/credential-provider-ini": "3.730.0", + "@aws-sdk/credential-provider-process": "3.730.0", + "@aws-sdk/credential-provider-sso": "3.730.0", + "@aws-sdk/credential-provider-web-identity": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@smithy/credential-provider-imds": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.730.0", + "@aws-sdk/core": "3.730.0", + "@aws-sdk/token-providers": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.730.0", + "@aws-sdk/nested-clients": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.723.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-logger": { + "version": "3.723.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.723.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.730.0", + "@smithy/core": "^3.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/nested-clients": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.730.0", + "@aws-sdk/middleware-host-header": "3.723.0", + "@aws-sdk/middleware-logger": "3.723.0", + "@aws-sdk/middleware-recursion-detection": "3.723.0", + "@aws-sdk/middleware-user-agent": "3.730.0", + "@aws-sdk/region-config-resolver": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.730.0", + "@aws-sdk/util-user-agent-browser": "3.723.0", + "@aws-sdk/util-user-agent-node": "3.730.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-cognito-identity/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.723.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/token-providers": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/types": { + "version": "3.723.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/util-endpoints": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.723.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/types": "^4.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-cognito-identity/node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/config-resolver": { + "version": "4.1.4", + "license": "Apache-2.0", + "dependencies": { + "@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-cognito-identity/node_modules/@smithy/core": { + "version": "3.5.3", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.8", + "@smithy/protocol-http": "^5.1.2", + "@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-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/credential-provider-imds": { + "version": "4.0.6", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/hash-node": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/invalid-dependency": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-content-length": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@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-cognito-identity/node_modules/@smithy/middleware-endpoint": { + "version": "4.1.11", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.5.3", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-retry": { + "version": "4.1.12", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.5", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-serde": { + "version": "4.0.8", + "license": "Apache-2.0", + "dependencies": { + "@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-cognito-identity/node_modules/@smithy/middleware-stack": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/node-config-provider": { + "version": "4.1.3", + "license": "Apache-2.0", + "dependencies": { + "@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-cognito-identity/node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "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-cognito-identity/node_modules/@smithy/property-provider": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/querystring-builder": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/service-error-classification": { + "version": "4.0.5", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/signature-v4": { + "version": "5.1.2", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/smithy-client": { + "version": "4.4.3", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.5.3", + "@smithy/middleware-endpoint": "^4.1.11", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@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-cognito-identity/node_modules/@smithy/types": { + "version": "4.3.1", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/url-parser": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-base64": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.19", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.19", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.4", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "license": "Apache-2.0", + "dependencies": { + "@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-cognito-identity/node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-retry": { + "version": "4.0.5", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.5", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-stream": { + "version": "4.2.2", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-datazone/-/client-datazone-3.848.0.tgz", + "integrity": "sha512-m9x9G6oQHUVJvt9JsTdU41/nimz11MMmQLptQVgIJcD6VHoHoVXppvPntK7GUkH0T6+0gw63RugGd7kB+xofBQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/credential-provider-node": "3.848.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.848.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.848.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.15", + "@smithy/middleware-retry": "^4.1.16", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@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.23", + "@smithy/util-defaults-mode-node": "^4.0.23", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-stream": "^4.2.3", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@aws-sdk/client-sso": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.848.0.tgz", + "integrity": "sha512-mD+gOwoeZQvbecVLGoCmY6pS7kg02BHesbtIxUj+PeBqYoZV5uLvjUOmuGfw1SfoSobKvS11urxC9S7zxU/Maw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.846.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.848.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.848.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.15", + "@smithy/middleware-retry": "^4.1.16", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@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.23", + "@smithy/util-defaults-mode-node": "^4.0.23", + "@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-datazone/node_modules/@aws-sdk/core": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.846.0.tgz", + "integrity": "sha512-7CX0pM906r4WSS68fCTNMTtBCSkTtf3Wggssmx13gD40gcWEZXsU00KzPp1bYheNRyPlAq3rE22xt4wLPXbuxA==", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.7.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.7", + "@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": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.846.0.tgz", + "integrity": "sha512-QuCQZET9enja7AWVISY+mpFrEIeHzvkx/JEEbHYzHhUkxcnC2Kq2c0bB7hDihGD0AZd3Xsm653hk1O97qu69zg==", + "dependencies": { + "@aws-sdk/core": "3.846.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-datazone/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.846.0.tgz", + "integrity": "sha512-Jh1iKUuepdmtreMYozV2ePsPcOF5W9p3U4tWhi3v6nDvz0GsBjzjAROW+BW8XMz9vAD3I9R+8VC3/aq63p5nlw==", + "dependencies": { + "@aws-sdk/core": "3.846.0", + "@aws-sdk/types": "3.840.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.848.0.tgz", + "integrity": "sha512-r6KWOG+En2xujuMhgZu7dzOZV3/M5U/5+PXrG8dLQ3rdPRB3vgp5tc56KMqLwm/EXKRzAOSuw/UE4HfNOAB8Hw==", + "dependencies": { + "@aws-sdk/core": "3.846.0", + "@aws-sdk/credential-provider-env": "3.846.0", + "@aws-sdk/credential-provider-http": "3.846.0", + "@aws-sdk/credential-provider-process": "3.846.0", + "@aws-sdk/credential-provider-sso": "3.848.0", + "@aws-sdk/credential-provider-web-identity": "3.848.0", + "@aws-sdk/nested-clients": "3.848.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-datazone/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.848.0.tgz", + "integrity": "sha512-AblNesOqdzrfyASBCo1xW3uweiSro4Kft9/htdxLeCVU1KVOnFWA5P937MNahViRmIQm2sPBCqL8ZG0u9lnh5g==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.846.0", + "@aws-sdk/credential-provider-http": "3.846.0", + "@aws-sdk/credential-provider-ini": "3.848.0", + "@aws-sdk/credential-provider-process": "3.846.0", + "@aws-sdk/credential-provider-sso": "3.848.0", + "@aws-sdk/credential-provider-web-identity": "3.848.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-datazone/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.846.0.tgz", + "integrity": "sha512-mEpwDYarJSH+CIXnnHN0QOe0MXI+HuPStD6gsv3z/7Q6ESl8KRWon3weFZCDnqpiJMUVavlDR0PPlAFg2MQoPg==", + "dependencies": { + "@aws-sdk/core": "3.846.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-datazone/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.848.0.tgz", + "integrity": "sha512-pozlDXOwJZL0e7w+dqXLgzVDB7oCx4WvtY0sk6l4i07uFliWF/exupb6pIehFWvTUcOvn5aFTTqcQaEzAD5Wsg==", + "dependencies": { + "@aws-sdk/client-sso": "3.848.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/token-providers": "3.848.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-datazone/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.848.0.tgz", + "integrity": "sha512-D1fRpwPxtVDhcSc/D71exa2gYweV+ocp4D3brF0PgFd//JR3XahZ9W24rVnTQwYEcK9auiBZB89Ltv+WbWN8qw==", + "dependencies": { + "@aws-sdk/core": "3.846.0", + "@aws-sdk/nested-clients": "3.848.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-datazone/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==", + "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-datazone/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==", + "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-datazone/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==", + "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-datazone/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.848.0.tgz", + "integrity": "sha512-rjMuqSWJEf169/ByxvBqfdei1iaduAnfolTshsZxwcmLIUtbYrFUmts0HrLQqsAG8feGPpDLHA272oPl+NTCCA==", + "dependencies": { + "@aws-sdk/core": "3.846.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@smithy/core": "^3.7.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-datazone/node_modules/@aws-sdk/nested-clients": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.848.0.tgz", + "integrity": "sha512-joLsyyo9u61jnZuyYzo1z7kmS7VgWRAkzSGESVzQHfOA1H2PYeUFek6vLT4+c9xMGrX/Z6B0tkRdzfdOPiatLg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.846.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.848.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.848.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.15", + "@smithy/middleware-retry": "^4.1.16", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@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.23", + "@smithy/util-defaults-mode-node": "^4.0.23", + "@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-datazone/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==", + "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-datazone/node_modules/@aws-sdk/token-providers": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.848.0.tgz", + "integrity": "sha512-oNPyM4+Di2Umu0JJRFSxDcKQ35+Chl/rAwD47/bS0cDPI8yrao83mLXLeDqpRPHyQW4sXlP763FZcuAibC0+mg==", + "dependencies": { + "@aws-sdk/core": "3.846.0", + "@aws-sdk/nested-clients": "3.848.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-datazone/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==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@aws-sdk/util-endpoints": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.848.0.tgz", + "integrity": "sha512-fY/NuFFCq/78liHvRyFKr+aqq1aA/uuVSANjzr5Ym8c+9Z3HRPE9OrExAHoMrZ6zC8tHerQwlsXYYH5XZ7H+ww==", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-endpoints": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/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==", + "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-datazone/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.848.0.tgz", + "integrity": "sha512-Zz1ft9NiLqbzNj/M0jVNxaoxI2F4tGXN0ZbZIj+KJ+PbJo+w5+Jo6d0UDAtbj3AEd79pjcCaP4OA9NTVzItUdw==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.848.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-datazone/node_modules/@aws-sdk/xml-builder": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", + "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/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-datazone/node_modules/@smithy/config-resolver": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", + "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "dependencies": { + "@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-datazone/node_modules/@smithy/core": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.7.1.tgz", + "integrity": "sha512-ExRCsHnXFtBPnM7MkfKBPcBBdHw1h/QS/cbNw4ho95qnyNHvnpmGbR39MIAv9KggTr5qSPxRSEL+hRXlyGyGQw==", + "dependencies": { + "@smithy/middleware-serde": "^4.0.8", + "@smithy/protocol-http": "^5.1.2", + "@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-stream": "^4.2.3", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/credential-provider-imds": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", + "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/fetch-http-handler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.0.tgz", + "integrity": "sha512-mADw7MS0bYe2OGKkHYMaqarOXuDwRbO6ArD91XhHcl2ynjGCFF+hvqf0LyQcYxkA1zaWjefSkU7Ne9mqgApSgQ==", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/hash-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", + "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/invalid-dependency": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", + "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/middleware-content-length": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", + "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/client-sso": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/middleware-endpoint": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.16.tgz", + "integrity": "sha512-plpa50PIGLqzMR2ANKAw2yOW5YKS626KYKqae3atwucbz4Ve4uQ9K9BEZxDLIFmCu7hKLcrq2zmj4a+PfmUV5w==", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", + "@smithy/core": "^3.7.1", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/middleware-retry": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.17.tgz", + "integrity": "sha512-gsCimeG6BApj0SBecwa1Be+Z+JOJe46iy3B3m3A8jKJHf7eIihP76Is4LwLrbJ1ygoS7Vg73lfqzejmLOrazUA==", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/smithy-client": "^4.4.8", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/middleware-serde": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", + "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/client-sts": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/middleware-stack": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", + "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.693.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/core": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/node-config-provider": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", + "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/core": "^2.5.2", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/property-provider": "^3.1.9", - "@smithy/protocol-http": "^4.1.6", - "@smithy/signature-v4": "^4.2.2", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/util-middleware": "^3.0.9", - "fast-xml-parser": "4.4.1", + "@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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/node-http-handler": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.0.tgz", + "integrity": "sha512-vqfSiHz2v8b3TTTrdXi03vNz1KLYYS3bhHCDv36FYDqxT7jvTll1mMnCrkD+gOvgwybuunh/2VmvOMqwBegxEg==", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/util-stream": "^3.3.0", + "@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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/property-provider": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", + "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-env": "3.693.0", - "@aws-sdk/credential-provider-http": "3.693.0", - "@aws-sdk/credential-provider-process": "3.693.0", - "@aws-sdk/credential-provider-sso": "3.693.0", - "@aws-sdk/credential-provider-web-identity": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/credential-provider-imds": "^3.2.6", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", + "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/querystring-builder": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", + "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.693.0", - "@aws-sdk/credential-provider-http": "3.693.0", - "@aws-sdk/credential-provider-ini": "3.693.0", - "@aws-sdk/credential-provider-process": "3.693.0", - "@aws-sdk/credential-provider-sso": "3.693.0", - "@aws-sdk/credential-provider-web-identity": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/credential-provider-imds": "^3.2.6", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/types": "^4.3.1", + "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", "dependencies": { - "@aws-sdk/client-sso": "3.693.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/token-providers": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/service-error-classification": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.6.tgz", + "integrity": "sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/types": "^3.7.0", + "@smithy/types": "^4.3.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", + "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", + "dependencies": { + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/signature-v4": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", + "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/smithy-client": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.8.tgz", + "integrity": "sha512-pcW691/lx7V54gE+dDGC26nxz8nrvnvRSCJaIYD6XLPpOInEZeKdV/SpSux+wqeQ4Ine7LJQu8uxMvobTIBK0w==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", + "@smithy/core": "^3.7.1", + "@smithy/middleware-endpoint": "^4.1.16", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.3", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/middleware-logger": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/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": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@smithy/core": "^2.5.2", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/types": "^3.7.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.9", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/token-providers": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.693.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/util-endpoints": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "@smithy/util-endpoints": "^2.1.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.24.tgz", + "integrity": "sha512-UkQNgaQ+bidw1MgdgPO1z1k95W/v8Ej/5o/T/Is8PiVUYPspl/ZxV6WO/8DrzZQu5ULnmpB9CDdMSRwgRc21AA==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.8", + "@smithy/types": "^4.3.1", "bowser": "^2.11.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.24.tgz", + "integrity": "sha512-phvGi/15Z4MpuQibTLOYIumvLdXb+XIJu8TA55voGgboln85jytA3wiD7CkUE8SNcWqkkb+uptZKPiuFouX/7g==", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.8", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", + "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-crt": ">=1.0.0" + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "dependencies": { + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", + "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", "dependencies": { + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-retry": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.6.tgz", + "integrity": "sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==", "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@smithy/util-utf8": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-stream": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.3.tgz", + "integrity": "sha512-cQn412DWHHFNKrQfbHY8vSFI3nTROY1aIKji9N0tpp8gUABRilr7wdf8fqBbSlXresobM+tQFNk6I+0LXK/YZg==", "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/client-datazone/node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/@aws-sdk/client-ec2": { + "version": "3.695.0", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -1553,6 +10416,7 @@ "@aws-sdk/middleware-host-header": "3.693.0", "@aws-sdk/middleware-logger": "3.693.0", "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-sdk-ec2": "3.693.0", "@aws-sdk/middleware-user-agent": "3.693.0", "@aws-sdk/region-config-resolver": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -1584,13 +10448,16 @@ "@smithy/util-middleware": "^3.0.9", "@smithy/util-retry": "^3.0.9", "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" + "@smithy/util-waiter": "^3.1.8", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/client-sso": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/client-sso": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1637,9 +10504,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/client-sso-oidc": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/client-sso-oidc": { "version": "3.693.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -1688,9 +10556,10 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/client-sts": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/client-sts": { "version": "3.693.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -1737,7 +10606,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/core": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/core": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1757,7 +10626,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-http": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-http": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1776,7 +10645,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-ini": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-ini": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1800,7 +10669,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-node": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1821,7 +10690,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-sso": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-sso": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1838,7 +10707,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-web-identity": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1855,7 +10724,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/middleware-host-header": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-host-header": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1868,7 +10737,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-logger": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1880,7 +10749,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1893,7 +10762,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/middleware-user-agent": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1909,7 +10778,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/region-config-resolver": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/region-config-resolver": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1924,7 +10793,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/token-providers": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/token-providers": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1941,7 +10810,7 @@ "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/util-endpoints": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/util-endpoints": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1954,7 +10823,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/util-user-agent-browser": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1964,7 +10833,7 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/util-user-agent-node": { + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -1986,7 +10855,7 @@ } } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-ec2/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -1996,7 +10865,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-ec2/node_modules/@smithy/util-buffer-from": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -2007,7 +10876,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-ec2/node_modules/@smithy/util-utf8": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -2018,9 +10887,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol": { + "node_modules/@aws-sdk/client-ecr": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ecr/-/client-ecr-3.693.0.tgz", + "integrity": "sha512-qBI06wo2VaQI/+Pb4GmZRVQMnXFr9B983nWWNhM6xzcYmfJKXbCW29syDVojiwp8/HPMOSqcKJzqIOqCWtN1Ug==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2063,17 +10933,16 @@ "@smithy/util-retry": "^3.0.9", "@smithy/util-utf8": "^3.0.0", "@smithy/util-waiter": "^3.1.8", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/client-sso": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/client-sso": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.693.0.tgz", + "integrity": "sha512-QEynrBC26x6TG9ZMzApR/kZ3lmt4lEIs2D+cHuDxt6fDGzahBUsQFBwJqhizzsM97JJI5YvmJhmihoYjdSSaXA==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2118,9 +10987,11 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/client-sso-oidc": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/client-sso-oidc": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.693.0.tgz", + "integrity": "sha512-UEDbYlYtK/e86OOMyFR4zEPyenIxDzO2DRdz3fwVW7RzZ94wfmSwBh/8skzPTuY1G7sI064cjHW0b0QG01Sdtg==", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2169,9 +11040,11 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/client-sts": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/client-sts": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.693.0.tgz", + "integrity": "sha512-4S2y7VEtvdnjJX4JPl4kDQlslxXEZFnC50/UXVUYSt/AMc5A/GgspFNA5FVz4E3Gwpfobbf23hR2NBF8AGvYoQ==", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2218,9 +11091,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/core": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/core": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.693.0.tgz", + "integrity": "sha512-v6Z/kWmLFqRLDPEwl9hJGhtTgIFHjZugSfF1Yqffdxf4n1AWgtHS7qSegakuMyN5pP4K2tvUD8qHJ+gGe2Bw2A==", "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/core": "^2.5.2", @@ -2238,9 +11112,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-http": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/credential-provider-http": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.693.0.tgz", + "integrity": "sha512-sL8MvwNJU7ZpD7/d2VVb3by1GknIJUxzTIgYtVkDVA/ojo+KRQSSHxcj0EWWXF5DTSh2Tm+LrEug3y1ZyKHsDA==", "dependencies": { "@aws-sdk/core": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -2257,9 +11132,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-ini": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/credential-provider-ini": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.693.0.tgz", + "integrity": "sha512-kvaa4mXhCCOuW7UQnBhYqYfgWmwy7WSBSDClutwSLPZvgrhYj2l16SD2lN4IfYdxARYMJJ1lFYp3/jJG/9Yk4Q==", "dependencies": { "@aws-sdk/core": "3.693.0", "@aws-sdk/credential-provider-env": "3.693.0", @@ -2281,9 +11157,10 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-node": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/credential-provider-node": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.693.0.tgz", + "integrity": "sha512-42WMsBjTNnjYxYuM3qD/Nq+8b7UdMopUq5OduMDxoM3mFTV6PXMMnfI4Z1TNnR4tYRvPXAnuNltF6xmjKbSJRA==", "dependencies": { "@aws-sdk/credential-provider-env": "3.693.0", "@aws-sdk/credential-provider-http": "3.693.0", @@ -2302,9 +11179,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-sso": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/credential-provider-sso": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.693.0.tgz", + "integrity": "sha512-479UlJxY+BFjj3pJFYUNC0DCMrykuG7wBAXfsvZqQxKUa83DnH5Q1ID/N2hZLkxjGd4ZW0AC3lTOMxFelGzzpQ==", "dependencies": { "@aws-sdk/client-sso": "3.693.0", "@aws-sdk/core": "3.693.0", @@ -2319,9 +11197,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-web-identity": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.693.0.tgz", + "integrity": "sha512-8LB210Pr6VeCiSb2hIra+sAH4KUBLyGaN50axHtIgufVK8jbKIctTZcVY5TO9Se+1107TsruzeXS7VeqVdJfFA==", "dependencies": { "@aws-sdk/core": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -2336,9 +11215,10 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/middleware-host-header": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/middleware-host-header": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.693.0.tgz", + "integrity": "sha512-BCki6sAZ5jYwIN/t3ElCiwerHad69ipHwPsDCxJQyeiOnJ8HG+lEpnVIfrnI8A0fLQNSF3Gtx6ahfBpKiv1Oug==", "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/protocol-http": "^4.1.6", @@ -2349,9 +11229,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/middleware-logger": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.693.0.tgz", + "integrity": "sha512-dXnXDPr+wIiJ1TLADACI1g9pkSB21KkMIko2u4CJ2JCBoxi5IqeTnVoa6YcC8GdFNVRl+PorZ3Zqfmf1EOTC6w==", "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/types": "^3.7.0", @@ -2361,9 +11242,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.693.0.tgz", + "integrity": "sha512-0LDmM+VxXp0u3rG0xQRWD/q6Ubi7G8I44tBPahevD5CaiDZTkmNTrVUf0VEJgVe0iCKBppACMBDkLB0/ETqkFw==", "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/protocol-http": "^4.1.6", @@ -2374,9 +11256,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/middleware-user-agent": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.693.0.tgz", + "integrity": "sha512-/KUq/KEpFFbQmNmpp7SpAtFAdViquDfD2W0QcG07zYBfz9MwE2ig48ALynXm5sMpRmnG7sJXjdvPtTsSVPfkiw==", "dependencies": { "@aws-sdk/core": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -2390,9 +11273,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/region-config-resolver": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/region-config-resolver": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.693.0.tgz", + "integrity": "sha512-YLUkMsUY0GLW/nfwlZ69cy1u07EZRmsv8Z9m0qW317/EZaVx59hcvmcvb+W4bFqj5E8YImTjoGfE4cZ0F9mkyw==", "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/node-config-provider": "^3.1.10", @@ -2405,9 +11289,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/token-providers": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/token-providers": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.693.0.tgz", + "integrity": "sha512-nDBTJMk1l/YmFULGfRbToOA2wjf+FkQT4dMgYCv+V9uSYsMzQj8A7Tha2dz9yv4vnQgYaEiErQ8d7HVyXcVEoA==", "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/property-provider": "^3.1.9", @@ -2422,9 +11307,10 @@ "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/util-endpoints": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/util-endpoints": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.693.0.tgz", + "integrity": "sha512-eo4F6DRQ/kxS3gxJpLRv+aDNy76DxQJL5B3DPzpr9Vkq0ygVoi4GT5oIZLVaAVIJmi6k5qq9dLsYZfWLUxJJSg==", "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/types": "^3.7.0", @@ -2435,9 +11321,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/util-user-agent-browser": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.693.0.tgz", + "integrity": "sha512-6EUfuKOujtddy18OLJUaXfKBgs+UcbZ6N/3QV4iOkubCUdeM1maIqs++B9bhCbWeaeF5ORizJw5FTwnyNjE/mw==", "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/types": "^3.7.0", @@ -2445,9 +11332,10 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/util-user-agent-node": { + "node_modules/@aws-sdk/client-ecr/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.693.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.693.0.tgz", + "integrity": "sha512-td0OVX8m5ZKiXtecIDuzY3Y3UZIzvxEr57Hp21NOwieqKCG2UeyQWWeGPv0FQaU7dpTkvFmVNI+tx9iB8V/Nhg==", "dependencies": { "@aws-sdk/middleware-user-agent": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -2467,9 +11355,10 @@ } } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-ecr/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", "dependencies": { "tslib": "^2.6.2" }, @@ -2477,9 +11366,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-ecr/node_modules/@smithy/util-buffer-from": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" @@ -2488,9 +11378,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-ecr/node_modules/@smithy/util-utf8": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" @@ -2499,1163 +11390,1157 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-eks/-/client-eks-3.936.0.tgz", + "integrity": "sha512-bdKxO/nj6VRiqHxgWBa/4fGdZOU5xyhRAZ4CB7Rn/oJy+PTYXZFNAQBQ3+aBZMvi5juNodKOBFInnOozZ+Hl1A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.682.0", - "@aws-sdk/client-sts": "3.682.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-node": "3.682.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.682.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.1.6", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@aws-sdk/core": "3.936.0", + "@aws-sdk/credential-provider-node": "3.936.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.936.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/client-sso": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.936.0.tgz", + "integrity": "sha512-0G73S2cDqYwJVvqL08eakj79MZG2QRaB56Ul8/Ps9oQxllr7DMI1IQ/N3j3xjxgpq/U36pkoFZ8aK1n7Sbr3IQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.936.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.936.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sso": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/core": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.936.0.tgz", + "integrity": "sha512-eGJ2ySUMvgtOziHhDRDLCrj473RJoL4J1vPjVM3NrKC/fF3/LoHjkut8AAnKmrW6a2uTzNKubigw8dEnpmpERw==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.682.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.936.0.tgz", + "integrity": "sha512-dKajFuaugEA5i9gCKzOaVy9uTeZcApE+7Z5wdcZ6j40523fY1a56khDAUYkCfwqa7sHci4ccmxBkAo+fW1RChA==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-node": "3.682.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.682.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", + "@aws-sdk/core": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.936.0.tgz", + "integrity": "sha512-5FguODLXG1tWx/x8fBxH+GVrk7Hey2LbXV5h9SFzYCx/2h50URBm0+9hndg0Rd23+xzYe14F6SI9HA9c1sPnjg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.682.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sts": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.936.0.tgz", + "integrity": "sha512-TbUv56ERQQujoHcLMcfL0Q6bVZfYF83gu/TjHkVkdSlHPOIKaG/mhE2XZSQzXv1cud6LlgeBbfzVAxJ+HPpffg==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.682.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-node": "3.682.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.682.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", + "@aws-sdk/core": "3.936.0", + "@aws-sdk/credential-provider-env": "3.936.0", + "@aws-sdk/credential-provider-http": "3.936.0", + "@aws-sdk/credential-provider-login": "3.936.0", + "@aws-sdk/credential-provider-process": "3.936.0", + "@aws-sdk/credential-provider-sso": "3.936.0", + "@aws-sdk/credential-provider-web-identity": "3.936.0", + "@aws-sdk/nested-clients": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/core": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.936.0.tgz", + "integrity": "sha512-rk/2PCtxX9xDsQW8p5Yjoca3StqmQcSfkmD7nQ61AqAHL1YgpSQWqHE+HjfGGiHDYKG7PvE33Ku2GyA7lEIJAw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/core": "^2.4.8", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/signature-v4": "^4.2.0", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/util-middleware": "^3.0.7", - "fast-xml-parser": "4.4.1", + "@aws-sdk/credential-provider-env": "3.936.0", + "@aws-sdk/credential-provider-http": "3.936.0", + "@aws-sdk/credential-provider-ini": "3.936.0", + "@aws-sdk/credential-provider-process": "3.936.0", + "@aws-sdk/credential-provider-sso": "3.936.0", + "@aws-sdk/credential-provider-web-identity": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.936.0.tgz", + "integrity": "sha512-GpA4AcHb96KQK2PSPUyvChvrsEKiLhQ5NWjeef2IZ3Jc8JoosiedYqp6yhZR+S8cTysuvx56WyJIJc8y8OTrLA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.5.0", + "@aws-sdk/core": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.936.0.tgz", + "integrity": "sha512-wHlEAJJvtnSyxTfNhN98JcU4taA1ED2JvuI2eePgawqBwS/Tzi0mhED1lvNIaWOkjfLd+nHALwszGrtJwEq4yQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/util-stream": "^3.1.9", + "@aws-sdk/client-sso": "3.936.0", + "@aws-sdk/core": "3.936.0", + "@aws-sdk/token-providers": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.936.0.tgz", + "integrity": "sha512-v3qHAuoODkoRXsAF4RG+ZVO6q2P9yYBT4GMpMEfU9wXVNn7AIfwZgTwzSUfnjNiGva5BKleWVpRpJ9DeuLFbUg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-env": "3.679.0", - "@aws-sdk/credential-provider-http": "3.679.0", - "@aws-sdk/credential-provider-process": "3.679.0", - "@aws-sdk/credential-provider-sso": "3.682.0", - "@aws-sdk/credential-provider-web-identity": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@aws-sdk/core": "3.936.0", + "@aws-sdk/nested-clients": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", + "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.682.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/middleware-logger": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", + "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.679.0", - "@aws-sdk/credential-provider-http": "3.679.0", - "@aws-sdk/credential-provider-ini": "3.682.0", - "@aws-sdk/credential-provider-process": "3.679.0", - "@aws-sdk/credential-provider-sso": "3.682.0", - "@aws-sdk/credential-provider-web-identity": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.936.0.tgz", + "integrity": "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@aws-sdk/types": "3.936.0", + "@aws/lambda-invoke-store": "^0.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.936.0.tgz", + "integrity": "sha512-YB40IPa7K3iaYX0lSnV9easDOLPLh+fJyUDF3BH8doX4i1AOSsYn86L4lVldmOaSX+DwiaqKHpvk4wPBdcIPWw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.682.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/token-providers": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@aws-sdk/core": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@smithy/core": "^3.18.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/nested-clients": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.936.0.tgz", + "integrity": "sha512-eyj2tz1XmDSLSZQ5xnB7cLTVKkSJnYAEoNDSUNhzWPxrBDYeJzIbatecOKceKCU8NBf8gWWZCK/CSY0mDxMO0A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.5.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.936.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.936.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", + "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/token-providers": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.936.0.tgz", + "integrity": "sha512-vvw8+VXk0I+IsoxZw0mX9TMJawUJvEsg3EF7zcCSetwhNPAU8Xmlhv7E/sN/FgSmm7b7DsqKoW6rVtQiCs1PWQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.936.0", + "@aws-sdk/nested-clients": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/util-endpoints": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.936.0.tgz", + "integrity": "sha512-XOEc7PF9Op00pWV2AYCGDSu5iHgYjIO53Py2VUQTIvP7SRCaCsXmA33mjBvC2Ms6FhSyWNa4aK4naUGIz0hQcw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.679.0" + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-logger": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.0.tgz", + "integrity": "sha512-D1jAmAZQYMoPiacfgNf7AWhg3DFN3Wq/vQv3WINt9znwjzHp2x+WzdJFxxj7xZL7V1U79As6G8f7PorMYWBKsQ==", "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/abort-controller": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", + "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/config-resolver": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", + "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@smithy/core": "^2.4.8", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/core": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.5.tgz", + "integrity": "sha512-6gnIz3h+PEPQGDj8MnRSjDvKBah042jEoPgjFGJ4iJLBE78L4lY/n98x14XyPF4u3lN179Ub/ZKFY5za9GeLQw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/types": "^3.5.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.7", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/token-providers": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/credential-provider-imds": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", + "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.679.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/types": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/fetch-http-handler": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", + "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-endpoints": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/hash-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", + "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", - "@smithy/util-endpoints": "^2.1.3", + "@smithy/types": "^4.9.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/invalid-dependency": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", + "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", - "bowser": "^2.11.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/types": "3.679.0", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.9", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/middleware-content-length": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", + "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.1.4", - "@smithy/querystring-builder": "^3.0.7", - "@smithy/types": "^3.5.0", - "@smithy/util-base64": "^3.0.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/middleware-endpoint": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.12.tgz", + "integrity": "sha512-9pAX/H+VQPzNbouhDhkW723igBMLgrI8OtX+++M7iKJgg/zY/Ig3i1e6seCcx22FWhE6Q/S61BRdi2wXBORT+A==", "license": "Apache-2.0", "dependencies": { + "@smithy/core": "^3.18.5", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/middleware-retry": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.12.tgz", + "integrity": "sha512-S4kWNKFowYd0lID7/DBqWHOQxmxlsf0jBaos9chQZUWTVOjSW1Ogyh8/ib5tM+agFDJ/TCxuCTvrnlc+9cIBcQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-utf8": { - "version": "3.0.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/middleware-serde": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", + "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/middleware-stack": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", + "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.682.0", - "@aws-sdk/client-sts": "3.682.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-node": "3.682.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.682.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/eventstream-serde-browser": "^3.0.10", - "@smithy/eventstream-serde-config-resolver": "^3.0.7", - "@smithy/eventstream-serde-node": "^3.0.9", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/client-sso": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/node-config-provider": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", + "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.682.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/node-http-handler": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", + "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-node": "3.682.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.682.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", + "@smithy/abort-controller": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.682.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/client-sts": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/property-provider": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", + "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.682.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-node": "3.682.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.682.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/core": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/protocol-http": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", + "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/core": "^2.4.8", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/signature-v4": "^4.2.0", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/util-middleware": "^3.0.7", - "fast-xml-parser": "4.4.1", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/querystring-builder": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", + "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.5.0", + "@smithy/types": "^4.9.0", + "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/querystring-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", + "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/util-stream": "^3.1.9", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/service-error-classification": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", + "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-env": "3.679.0", - "@aws-sdk/credential-provider-http": "3.679.0", - "@aws-sdk/credential-provider-process": "3.679.0", - "@aws-sdk/credential-provider-sso": "3.682.0", - "@aws-sdk/credential-provider-web-identity": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" + "@smithy/types": "^4.9.0" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.682.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", + "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.679.0", - "@aws-sdk/credential-provider-http": "3.679.0", - "@aws-sdk/credential-provider-ini": "3.682.0", - "@aws-sdk/credential-provider-process": "3.679.0", - "@aws-sdk/credential-provider-sso": "3.682.0", - "@aws-sdk/credential-provider-web-identity": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/signature-v4": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", + "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/smithy-client": { + "version": "4.9.8", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.8.tgz", + "integrity": "sha512-8xgq3LgKDEFoIrLWBho/oYKyWByw9/corz7vuh1upv7ZBm0ZMjGYBhbn6v643WoIqA9UTcx5A5htEp/YatUwMA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.682.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/token-providers": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@smithy/core": "^3.18.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/types": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", + "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.679.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/url-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", + "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", + "@smithy/querystring-parser": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-logger": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@smithy/core": "^2.4.8", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/types": "^3.5.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.7", + "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/token-providers": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.679.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/types": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.11.tgz", + "integrity": "sha512-yHv+r6wSQXEXTPVCIQTNmXVWs7ekBTpMVErjqZoWkYN75HIFN5y9+/+sYOejfAuvxWGvgzgxbTHa/oz61YTbKw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/util-endpoints": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.14.tgz", + "integrity": "sha512-ljZN3iRvaJUgulfvobIuG97q1iUuCMrvXAlkZ4msY+ZuVHQHDIqn7FKZCEj+bx8omz6kF5yQXms/xhzjIO5XiA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", - "@smithy/util-endpoints": "^2.1.3", + "@smithy/config-resolver": "^4.4.3", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-endpoints": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", + "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", - "bowser": "^2.11.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/types": "3.679.0", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-middleware": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", + "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.9", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-retry": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", + "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.1.4", - "@smithy/querystring-builder": "^3.0.7", - "@smithy/types": "^3.5.0", - "@smithy/util-base64": "^3.0.0", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-stream": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", + "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", "license": "Apache-2.0", "dependencies": { + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@smithy/util-utf8": { - "version": "3.0.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-eks/node_modules/@smithy/util-waiter": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.5.tgz", + "integrity": "sha512-Dbun99A3InifQdIrsXZ+QLcC0PGBPAdrl4cj1mTgJvyc9N2zf7QSxg8TBkzsCmGJdE3TLbO9ycwpY0EkWahQ/g==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/token-providers": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/abort-controller": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-eks/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/client-eks/node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@aws-sdk/client-glue": { + "version": "3.852.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-glue/-/client-glue-3.852.0.tgz", + "integrity": "sha512-5IyZt/gKr0NoUHWGM112ikXrZs+VsA/09bwKDmp4/j250tfaZqgC1zhfBNFkyNisj1JQ0XYjwfzkLnYWlT3Pyw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.758.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.5", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-retry": "^4.0.7", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/credential-provider-node": "3.848.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.848.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.848.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.15", + "@smithy/middleware-retry": "^4.1.16", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@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.7", - "@smithy/util-defaults-mode-node": "^4.0.7", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", + "@smithy/util-defaults-mode-browser": "^4.0.23", + "@smithy/util-defaults-mode-node": "^4.0.23", + "@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" }, @@ -3663,48 +12548,47 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/client-sso": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.848.0.tgz", + "integrity": "sha512-mD+gOwoeZQvbecVLGoCmY6pS7kg02BHesbtIxUj+PeBqYoZV5uLvjUOmuGfw1SfoSobKvS11urxC9S7zxU/Maw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-node": "3.758.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.758.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.5", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-retry": "^4.0.7", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", + "@aws-sdk/core": "3.846.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.848.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.848.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.15", + "@smithy/middleware-retry": "^4.1.16", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@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.7", - "@smithy/util-defaults-mode-node": "^4.0.7", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", + "@smithy/util-defaults-mode-browser": "^4.0.23", + "@smithy/util-defaults-mode-node": "^4.0.23", + "@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" }, @@ -3712,187 +12596,260 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/core": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/core": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.846.0.tgz", + "integrity": "sha512-7CX0pM906r4WSS68fCTNMTtBCSkTtf3Wggssmx13gD40gcWEZXsU00KzPp1bYheNRyPlAq3rE22xt4wLPXbuxA==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.7.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.7", + "@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": "5.2.5", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.846.0.tgz", + "integrity": "sha512-QuCQZET9enja7AWVISY+mpFrEIeHzvkx/JEEbHYzHhUkxcnC2Kq2c0bB7hDihGD0AZd3Xsm653hk1O97qu69zg==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.846.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.846.0.tgz", + "integrity": "sha512-Jh1iKUuepdmtreMYozV2ePsPcOF5W9p3U4tWhi3v6nDvz0GsBjzjAROW+BW8XMz9vAD3I9R+8VC3/aq63p5nlw==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/types": "3.840.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.3", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.848.0.tgz", + "integrity": "sha512-r6KWOG+En2xujuMhgZu7dzOZV3/M5U/5+PXrG8dLQ3rdPRB3vgp5tc56KMqLwm/EXKRzAOSuw/UE4HfNOAB8Hw==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/credential-provider-env": "3.846.0", + "@aws-sdk/credential-provider-http": "3.846.0", + "@aws-sdk/credential-provider-process": "3.846.0", + "@aws-sdk/credential-provider-sso": "3.848.0", + "@aws-sdk/credential-provider-web-identity": "3.848.0", + "@aws-sdk/nested-clients": "3.848.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.848.0.tgz", + "integrity": "sha512-AblNesOqdzrfyASBCo1xW3uweiSro4Kft9/htdxLeCVU1KVOnFWA5P937MNahViRmIQm2sPBCqL8ZG0u9lnh5g==", "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@smithy/core": "^3.1.5", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/credential-provider-env": "3.846.0", + "@aws-sdk/credential-provider-http": "3.846.0", + "@aws-sdk/credential-provider-ini": "3.848.0", + "@aws-sdk/credential-provider-process": "3.846.0", + "@aws-sdk/credential-provider-sso": "3.848.0", + "@aws-sdk/credential-provider-web-identity": "3.848.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.846.0.tgz", + "integrity": "sha512-mEpwDYarJSH+CIXnnHN0QOe0MXI+HuPStD6gsv3z/7Q6ESl8KRWon3weFZCDnqpiJMUVavlDR0PPlAFg2MQoPg==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", + "@aws-sdk/core": "3.846.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.848.0.tgz", + "integrity": "sha512-pozlDXOwJZL0e7w+dqXLgzVDB7oCx4WvtY0sk6l4i07uFliWF/exupb6pIehFWvTUcOvn5aFTTqcQaEzAD5Wsg==", "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/client-sso": "3.848.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/token-providers": "3.848.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-endpoints": { - "version": "3.743.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.848.0.tgz", + "integrity": "sha512-D1fRpwPxtVDhcSc/D71exa2gYweV+ocp4D3brF0PgFd//JR3XahZ9W24rVnTQwYEcK9auiBZB89Ltv+WbWN8qw==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/nested-clients": "3.848.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/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==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", + "@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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/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==", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", + "@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-glue/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==", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/config-resolver": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.848.0.tgz", + "integrity": "sha512-rjMuqSWJEf169/ByxvBqfdei1iaduAnfolTshsZxwcmLIUtbYrFUmts0HrLQqsAG8feGPpDLHA272oPl+NTCCA==", "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@smithy/core": "^3.7.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/core": { - "version": "3.1.5", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/nested-clients": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.848.0.tgz", + "integrity": "sha512-joLsyyo9u61jnZuyYzo1z7kmS7VgWRAkzSGESVzQHfOA1H2PYeUFek6vLT4+c9xMGrX/Z6B0tkRdzfdOPiatLg==", "dependencies": { - "@smithy/middleware-serde": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.846.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.848.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.848.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.15", + "@smithy/middleware-retry": "^4.1.16", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@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-middleware": "^4.0.1", - "@smithy/util-stream": "^4.1.2", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.23", + "@smithy/util-defaults-mode-node": "^4.0.23", + "@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" }, @@ -3900,930 +12857,1362 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/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==", "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.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", + "@smithy/util-middleware": "^4.0.4", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/hash-node": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/token-providers": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.848.0.tgz", + "integrity": "sha512-oNPyM4+Di2Umu0JJRFSxDcKQ35+Chl/rAwD47/bS0cDPI8yrao83mLXLeDqpRPHyQW4sXlP763FZcuAibC0+mg==", "dependencies": { - "@smithy/types": "^4.1.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/nested-clients": "3.848.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/invalid-dependency": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/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==", "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-content-length": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/util-endpoints": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.848.0.tgz", + "integrity": "sha512-fY/NuFFCq/78liHvRyFKr+aqq1aA/uuVSANjzr5Ym8c+9Z3HRPE9OrExAHoMrZ6zC8tHerQwlsXYYH5XZ7H+ww==", "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-endpoints": "^3.0.6", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-endpoint": { + "node_modules/@aws-sdk/client-glue/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==", + "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-glue/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.848.0.tgz", + "integrity": "sha512-Zz1ft9NiLqbzNj/M0jVNxaoxI2F4tGXN0ZbZIj+KJ+PbJo+w5+Jo6d0UDAtbj3AEd79pjcCaP4OA9NTVzItUdw==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.848.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-glue/node_modules/@aws-sdk/xml-builder": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", + "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-glue/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-glue/node_modules/@smithy/config-resolver": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", + "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "dependencies": { + "@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-glue/node_modules/@smithy/core": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.7.2.tgz", + "integrity": "sha512-JoLw59sT5Bm8SAjFCYZyuCGxK8y3vovmoVbZWLDPTH5XpPEIwpFd9m90jjVMwoypDuB/SdVgje5Y4T7w50lJaw==", + "dependencies": { + "@smithy/middleware-serde": "^4.0.8", + "@smithy/protocol-http": "^5.1.2", + "@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-stream": "^4.2.3", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/credential-provider-imds": { "version": "4.0.6", - "license": "Apache-2.0", - "peer": true, + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", + "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-middleware": "^4.0.1", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-retry": { - "version": "4.0.7", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/fetch-http-handler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.0.tgz", + "integrity": "sha512-mADw7MS0bYe2OGKkHYMaqarOXuDwRbO6ArD91XhHcl2ynjGCFF+hvqf0LyQcYxkA1zaWjefSkU7Ne9mqgApSgQ==", "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/service-error-classification": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/hash-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", + "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/invalid-dependency": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", + "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/middleware-content-length": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", + "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", "dependencies": { - "@smithy/abort-controller": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/property-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/middleware-endpoint": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.17.tgz", + "integrity": "sha512-S3hSGLKmHG1m35p/MObQCBCdRsrpbPU8B129BVzRqRfDvQqPMQ14iO4LyRw+7LNizYc605COYAcjqgawqi+6jA==", "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/core": "^3.7.2", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/protocol-http": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/middleware-retry": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.18.tgz", + "integrity": "sha512-bYLZ4DkoxSsPxpdmeapvAKy7rM5+25gR7PGxq2iMiecmbrRGBHj9s75N74Ylg+aBiw9i5jIowC/cLU2NR0qH8w==", "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/smithy-client": "^4.4.9", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/middleware-serde": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", + "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", "dependencies": { - "@smithy/types": "^4.1.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/signature-v4": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/middleware-stack": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", + "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/smithy-client": { - "version": "4.1.6", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/node-config-provider": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", + "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/types": { + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/node-http-handler": { "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.0.tgz", + "integrity": "sha512-vqfSiHz2v8b3TTTrdXi03vNz1KLYYS3bhHCDv36FYDqxT7jvTll1mMnCrkD+gOvgwybuunh/2VmvOMqwBegxEg==", "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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/url-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/property-provider": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", + "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", "dependencies": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-base64": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", + "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/querystring-builder": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", + "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", "dependencies": { + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-config-provider": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/service-error-classification": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.6.tgz", + "integrity": "sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==", "dependencies": { - "tslib": "^2.6.2" + "@smithy/types": "^4.3.1" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.7", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", + "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.7", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/signature-v4": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", + "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", "dependencies": { - "@smithy/config-resolver": "^4.0.1", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-endpoints": { - "version": "3.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/smithy-client": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.9.tgz", + "integrity": "sha512-mbMg8mIUAWwMmb74LoYiArP04zWElPzDoA1jVOp3or0cjlDMgoS6WTC3QXK0Vxoc9I4zdrX0tq6qsOmaIoTWEQ==", "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/core": "^3.7.2", + "@smithy/middleware-endpoint": "^4.1.17", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.3", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-middleware": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/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": { - "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-retry": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", "dependencies": { - "@smithy/service-error-classification": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-base64": { "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", "dependencies": { "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/core": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", + "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.25.tgz", + "integrity": "sha512-pxEWsxIsOPLfKNXvpgFHBGFC3pKYKUFhrud1kyooO9CJai6aaKDHfT10Mi5iiipPXN/JhKAu3qX9o75+X85OdQ==", "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@smithy/core": "^3.1.5", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.9", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.25.tgz", + "integrity": "sha512-+w4n4hKFayeCyELZLfsSQG5mCC3TwSkmRHv4+el5CzFU8ToQpYGhpV7mrRzqlwKkntlPilT1HJy1TVeEvEjWOQ==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", + "@smithy/config-resolver": "^4.1.4", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.9", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", + "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", "dependencies": { - "@smithy/types": "^4.1.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-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { - "version": "3.743.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", + "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-retry": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.6.tgz", + "integrity": "sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-stream": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.3.tgz", + "integrity": "sha512-cQn412DWHHFNKrQfbHY8vSFI3nTROY1aIKji9N0tpp8gUABRilr7wdf8fqBbSlXresobM+tQFNk6I+0LXK/YZg==", + "dependencies": { + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/config-resolver": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/core": { - "version": "3.1.5", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", "dependencies": { - "@smithy/middleware-serde": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-stream": "^4.1.2", - "@smithy/util-utf8": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-glue/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/client-glue/node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/@aws-sdk/client-iam": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/hash-node": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/invalid-dependency": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-content-length": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/core": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-middleware": "^4.0.1", + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-retry": { - "version": "4.0.7", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/service-error-classification": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/abort-controller": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/property-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/signature-v4": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/smithy-client": { - "version": "4.1.6", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/url-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-base64": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-config-provider": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.7", + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", + "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.7", + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/util-utf8": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/config-resolver": "^4.0.1", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-endpoints": { - "version": "3.0.1", + "node_modules/@aws-sdk/client-iot": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-iot/-/client-iot-3.693.0.tgz", + "integrity": "sha512-0EOKH6CjDHMdE1NSDdtZ8/zov+Xf1MovWvAeQGs76ec4mL2VWP5HvePjjdkGoOo0KC9k/AqOVVc0UOZjK0iCQw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-middleware": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/client-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.693.0.tgz", + "integrity": "sha512-QEynrBC26x6TG9ZMzApR/kZ3lmt4lEIs2D+cHuDxt6fDGzahBUsQFBwJqhizzsM97JJI5YvmJhmihoYjdSSaXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-retry": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.693.0.tgz", + "integrity": "sha512-UEDbYlYtK/e86OOMyFR4zEPyenIxDzO2DRdz3fwVW7RzZ94wfmSwBh/8skzPTuY1G7sI064cjHW0b0QG01Sdtg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/service-error-classification": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.693.0.tgz", + "integrity": "sha512-4S2y7VEtvdnjJX4JPl4kDQlslxXEZFnC50/UXVUYSt/AMc5A/GgspFNA5FVz4E3Gwpfobbf23hR2NBF8AGvYoQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/core": { + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/core": { "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.693.0.tgz", + "integrity": "sha512-v6Z/kWmLFqRLDPEwl9hJGhtTgIFHjZugSfF1Yqffdxf4n1AWgtHS7qSegakuMyN5pP4K2tvUD8qHJ+gGe2Bw2A==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.692.0", @@ -4835,1160 +14224,1861 @@ "@smithy/smithy-client": "^3.4.3", "@smithy/types": "^3.7.0", "@smithy/util-middleware": "^3.0.9", - "fast-xml-parser": "4.4.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/types": "^4.1.0", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@aws-sdk/core": { - "version": "3.758.0", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.693.0.tgz", + "integrity": "sha512-sL8MvwNJU7ZpD7/d2VVb3by1GknIJUxzTIgYtVkDVA/ojo+KRQSSHxcj0EWWXF5DTSh2Tm+LrEug3y1ZyKHsDA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@aws-sdk/types": { - "version": "3.734.0", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.693.0.tgz", + "integrity": "sha512-kvaa4mXhCCOuW7UQnBhYqYfgWmwy7WSBSDClutwSLPZvgrhYj2l16SD2lN4IfYdxARYMJJ1lFYp3/jJG/9Yk4Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/core": { - "version": "3.1.5", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.693.0.tgz", + "integrity": "sha512-42WMsBjTNnjYxYuM3qD/Nq+8b7UdMopUq5OduMDxoM3mFTV6PXMMnfI4Z1TNnR4tYRvPXAnuNltF6xmjKbSJRA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/middleware-serde": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-stream": "^4.1.2", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.693.0.tgz", + "integrity": "sha512-479UlJxY+BFjj3pJFYUNC0DCMrykuG7wBAXfsvZqQxKUa83DnH5Q1ID/N2hZLkxjGd4ZW0AC3lTOMxFelGzzpQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-middleware": "^4.0.1", + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.693.0.tgz", + "integrity": "sha512-8LB210Pr6VeCiSb2hIra+sAH4KUBLyGaN50axHtIgufVK8jbKIctTZcVY5TO9Se+1107TsruzeXS7VeqVdJfFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.693.0.tgz", + "integrity": "sha512-BCki6sAZ5jYwIN/t3ElCiwerHad69ipHwPsDCxJQyeiOnJ8HG+lEpnVIfrnI8A0fLQNSF3Gtx6ahfBpKiv1Oug==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.693.0.tgz", + "integrity": "sha512-dXnXDPr+wIiJ1TLADACI1g9pkSB21KkMIko2u4CJ2JCBoxi5IqeTnVoa6YcC8GdFNVRl+PorZ3Zqfmf1EOTC6w==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/property-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.693.0.tgz", + "integrity": "sha512-0LDmM+VxXp0u3rG0xQRWD/q6Ubi7G8I44tBPahevD5CaiDZTkmNTrVUf0VEJgVe0iCKBppACMBDkLB0/ETqkFw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.693.0.tgz", + "integrity": "sha512-/KUq/KEpFFbQmNmpp7SpAtFAdViquDfD2W0QcG07zYBfz9MwE2ig48ALynXm5sMpRmnG7sJXjdvPtTsSVPfkiw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.693.0.tgz", + "integrity": "sha512-YLUkMsUY0GLW/nfwlZ69cy1u07EZRmsv8Z9m0qW317/EZaVx59hcvmcvb+W4bFqj5E8YImTjoGfE4cZ0F9mkyw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/signature-v4": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.693.0.tgz", + "integrity": "sha512-nDBTJMk1l/YmFULGfRbToOA2wjf+FkQT4dMgYCv+V9uSYsMzQj8A7Tha2dz9yv4vnQgYaEiErQ8d7HVyXcVEoA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/smithy-client": { - "version": "4.1.6", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.693.0.tgz", + "integrity": "sha512-eo4F6DRQ/kxS3gxJpLRv+aDNy76DxQJL5B3DPzpr9Vkq0ygVoi4GT5oIZLVaAVIJmi6k5qq9dLsYZfWLUxJJSg==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.693.0.tgz", + "integrity": "sha512-6EUfuKOujtddy18OLJUaXfKBgs+UcbZ6N/3QV4iOkubCUdeM1maIqs++B9bhCbWeaeF5ORizJw5FTwnyNjE/mw==", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/url-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iot/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.693.0.tgz", + "integrity": "sha512-td0OVX8m5ZKiXtecIDuzY3Y3UZIzvxEr57Hp21NOwieqKCG2UeyQWWeGPv0FQaU7dpTkvFmVNI+tx9iB8V/Nhg==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iot/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/util-middleware": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iot/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/util-utf8": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iot/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.758.0", + "node_modules/@aws-sdk/client-iotsecuretunneling": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-iotsecuretunneling/-/client-iotsecuretunneling-3.693.0.tgz", + "integrity": "sha512-f9p5/TgVQsko0FlYIj9UKAVSfgPF4GgoKGVOI3Gx6XpynYwideGxItq3v0ExoAzpaohq6zRKleqA68o/T1TqXQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@aws-sdk/core": { - "version": "3.758.0", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/client-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.693.0.tgz", + "integrity": "sha512-QEynrBC26x6TG9ZMzApR/kZ3lmt4lEIs2D+cHuDxt6fDGzahBUsQFBwJqhizzsM97JJI5YvmJhmihoYjdSSaXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@aws-sdk/types": { - "version": "3.734.0", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.693.0.tgz", + "integrity": "sha512-UEDbYlYtK/e86OOMyFR4zEPyenIxDzO2DRdz3fwVW7RzZ94wfmSwBh/8skzPTuY1G7sI064cjHW0b0QG01Sdtg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/core": { - "version": "3.1.5", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.693.0.tgz", + "integrity": "sha512-4S2y7VEtvdnjJX4JPl4kDQlslxXEZFnC50/UXVUYSt/AMc5A/GgspFNA5FVz4E3Gwpfobbf23hR2NBF8AGvYoQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/middleware-serde": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-stream": "^4.1.2", - "@smithy/util-utf8": "^4.0.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.693.0.tgz", + "integrity": "sha512-v6Z/kWmLFqRLDPEwl9hJGhtTgIFHjZugSfF1Yqffdxf4n1AWgtHS7qSegakuMyN5pP4K2tvUD8qHJ+gGe2Bw2A==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.693.0.tgz", + "integrity": "sha512-sL8MvwNJU7ZpD7/d2VVb3by1GknIJUxzTIgYtVkDVA/ojo+KRQSSHxcj0EWWXF5DTSh2Tm+LrEug3y1ZyKHsDA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-middleware": "^4.0.1", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.693.0.tgz", + "integrity": "sha512-kvaa4mXhCCOuW7UQnBhYqYfgWmwy7WSBSDClutwSLPZvgrhYj2l16SD2lN4IfYdxARYMJJ1lFYp3/jJG/9Yk4Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.693.0.tgz", + "integrity": "sha512-42WMsBjTNnjYxYuM3qD/Nq+8b7UdMopUq5OduMDxoM3mFTV6PXMMnfI4Z1TNnR4tYRvPXAnuNltF6xmjKbSJRA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.693.0.tgz", + "integrity": "sha512-479UlJxY+BFjj3pJFYUNC0DCMrykuG7wBAXfsvZqQxKUa83DnH5Q1ID/N2hZLkxjGd4ZW0AC3lTOMxFelGzzpQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.693.0.tgz", + "integrity": "sha512-8LB210Pr6VeCiSb2hIra+sAH4KUBLyGaN50axHtIgufVK8jbKIctTZcVY5TO9Se+1107TsruzeXS7VeqVdJfFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/abort-controller": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/property-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.693.0.tgz", + "integrity": "sha512-BCki6sAZ5jYwIN/t3ElCiwerHad69ipHwPsDCxJQyeiOnJ8HG+lEpnVIfrnI8A0fLQNSF3Gtx6ahfBpKiv1Oug==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.693.0.tgz", + "integrity": "sha512-dXnXDPr+wIiJ1TLADACI1g9pkSB21KkMIko2u4CJ2JCBoxi5IqeTnVoa6YcC8GdFNVRl+PorZ3Zqfmf1EOTC6w==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.693.0.tgz", + "integrity": "sha512-0LDmM+VxXp0u3rG0xQRWD/q6Ubi7G8I44tBPahevD5CaiDZTkmNTrVUf0VEJgVe0iCKBppACMBDkLB0/ETqkFw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/signature-v4": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.693.0.tgz", + "integrity": "sha512-/KUq/KEpFFbQmNmpp7SpAtFAdViquDfD2W0QcG07zYBfz9MwE2ig48ALynXm5sMpRmnG7sJXjdvPtTsSVPfkiw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/smithy-client": { - "version": "4.1.6", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.693.0.tgz", + "integrity": "sha512-YLUkMsUY0GLW/nfwlZ69cy1u07EZRmsv8Z9m0qW317/EZaVx59hcvmcvb+W4bFqj5E8YImTjoGfE4cZ0F9mkyw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.693.0.tgz", + "integrity": "sha512-nDBTJMk1l/YmFULGfRbToOA2wjf+FkQT4dMgYCv+V9uSYsMzQj8A7Tha2dz9yv4vnQgYaEiErQ8d7HVyXcVEoA==", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/url-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.693.0.tgz", + "integrity": "sha512-eo4F6DRQ/kxS3gxJpLRv+aDNy76DxQJL5B3DPzpr9Vkq0ygVoi4GT5oIZLVaAVIJmi6k5qq9dLsYZfWLUxJJSg==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/util-base64": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.693.0.tgz", + "integrity": "sha512-6EUfuKOujtddy18OLJUaXfKBgs+UcbZ6N/3QV4iOkubCUdeM1maIqs++B9bhCbWeaeF5ORizJw5FTwnyNjE/mw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.693.0.tgz", + "integrity": "sha512-td0OVX8m5ZKiXtecIDuzY3Y3UZIzvxEr57Hp21NOwieqKCG2UeyQWWeGPv0FQaU7dpTkvFmVNI+tx9iB8V/Nhg==", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/util-middleware": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/util-utf8": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.758.0", + "node_modules/@aws-sdk/client-iotsecuretunneling/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/credential-provider-env": "3.758.0", - "@aws-sdk/credential-provider-http": "3.758.0", - "@aws-sdk/credential-provider-ini": "3.758.0", - "@aws-sdk/credential-provider-process": "3.758.0", - "@aws-sdk/credential-provider-sso": "3.758.0", - "@aws-sdk/credential-provider-web-identity": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/types": { - "version": "3.734.0", + "node_modules/@aws-sdk/client-lambda": { + "version": "3.637.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.637.0", + "@aws-sdk/client-sts": "3.637.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/eventstream-serde-browser": "^3.0.6", + "@smithy/eventstream-serde-config-resolver": "^3.0.3", + "@smithy/eventstream-serde-node": "^3.0.5", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-stream": "^3.1.3", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-node/node_modules/@smithy/property-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/types": { + "version": "3.609.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-node/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-node/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.758.0", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-utf8": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@aws-sdk/core": { - "version": "3.758.0", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", + "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@aws-sdk/types": { - "version": "3.734.0", + "node_modules/@aws-sdk/client-redshift": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-redshift/-/client-redshift-3.693.0.tgz", + "integrity": "sha512-k+4emXXK7iOOYjTAU+Erj5RVxu68Hi6iI48h5r8iNMhWRUMqUq346tK5qkD4C4x9SzJu5j0WgPWpVUiHu8ufDw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/core": { - "version": "3.1.5", + "node_modules/@aws-sdk/client-redshift-data": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-redshift-data/-/client-redshift-data-3.693.0.tgz", + "integrity": "sha512-uG5LdlXz80KcauRIucMdiRSQJ2WutewQRHpcTQW4vFUf/kEhUha5fD9FMn+/eJ1NFA2N8hv64vhpzGvu7EiP6Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/middleware-serde": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-stream": "^4.1.2", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/client-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.693.0.tgz", + "integrity": "sha512-QEynrBC26x6TG9ZMzApR/kZ3lmt4lEIs2D+cHuDxt6fDGzahBUsQFBwJqhizzsM97JJI5YvmJhmihoYjdSSaXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-middleware": "^4.0.1", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.693.0.tgz", + "integrity": "sha512-UEDbYlYtK/e86OOMyFR4zEPyenIxDzO2DRdz3fwVW7RzZ94wfmSwBh/8skzPTuY1G7sI064cjHW0b0QG01Sdtg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.693.0.tgz", + "integrity": "sha512-4S2y7VEtvdnjJX4JPl4kDQlslxXEZFnC50/UXVUYSt/AMc5A/GgspFNA5FVz4E3Gwpfobbf23hR2NBF8AGvYoQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.693.0.tgz", + "integrity": "sha512-v6Z/kWmLFqRLDPEwl9hJGhtTgIFHjZugSfF1Yqffdxf4n1AWgtHS7qSegakuMyN5pP4K2tvUD8qHJ+gGe2Bw2A==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/property-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.693.0.tgz", + "integrity": "sha512-sL8MvwNJU7ZpD7/d2VVb3by1GknIJUxzTIgYtVkDVA/ojo+KRQSSHxcj0EWWXF5DTSh2Tm+LrEug3y1ZyKHsDA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.693.0.tgz", + "integrity": "sha512-kvaa4mXhCCOuW7UQnBhYqYfgWmwy7WSBSDClutwSLPZvgrhYj2l16SD2lN4IfYdxARYMJJ1lFYp3/jJG/9Yk4Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.693.0.tgz", + "integrity": "sha512-42WMsBjTNnjYxYuM3qD/Nq+8b7UdMopUq5OduMDxoM3mFTV6PXMMnfI4Z1TNnR4tYRvPXAnuNltF6xmjKbSJRA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/signature-v4": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.693.0.tgz", + "integrity": "sha512-479UlJxY+BFjj3pJFYUNC0DCMrykuG7wBAXfsvZqQxKUa83DnH5Q1ID/N2hZLkxjGd4ZW0AC3lTOMxFelGzzpQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/smithy-client": { - "version": "4.1.6", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.693.0.tgz", + "integrity": "sha512-8LB210Pr6VeCiSb2hIra+sAH4KUBLyGaN50axHtIgufVK8jbKIctTZcVY5TO9Se+1107TsruzeXS7VeqVdJfFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.693.0.tgz", + "integrity": "sha512-BCki6sAZ5jYwIN/t3ElCiwerHad69ipHwPsDCxJQyeiOnJ8HG+lEpnVIfrnI8A0fLQNSF3Gtx6ahfBpKiv1Oug==", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/url-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.693.0.tgz", + "integrity": "sha512-dXnXDPr+wIiJ1TLADACI1g9pkSB21KkMIko2u4CJ2JCBoxi5IqeTnVoa6YcC8GdFNVRl+PorZ3Zqfmf1EOTC6w==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.693.0.tgz", + "integrity": "sha512-0LDmM+VxXp0u3rG0xQRWD/q6Ubi7G8I44tBPahevD5CaiDZTkmNTrVUf0VEJgVe0iCKBppACMBDkLB0/ETqkFw==", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/util-middleware": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.693.0.tgz", + "integrity": "sha512-/KUq/KEpFFbQmNmpp7SpAtFAdViquDfD2W0QcG07zYBfz9MwE2ig48ALynXm5sMpRmnG7sJXjdvPtTsSVPfkiw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/util-utf8": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.693.0.tgz", + "integrity": "sha512-YLUkMsUY0GLW/nfwlZ69cy1u07EZRmsv8Z9m0qW317/EZaVx59hcvmcvb+W4bFqj5E8YImTjoGfE4cZ0F9mkyw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.758.0", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.693.0.tgz", + "integrity": "sha512-nDBTJMk1l/YmFULGfRbToOA2wjf+FkQT4dMgYCv+V9uSYsMzQj8A7Tha2dz9yv4vnQgYaEiErQ8d7HVyXcVEoA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/client-sso": "3.758.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/token-providers": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/core": { - "version": "3.758.0", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.693.0.tgz", + "integrity": "sha512-eo4F6DRQ/kxS3gxJpLRv+aDNy76DxQJL5B3DPzpr9Vkq0ygVoi4GT5oIZLVaAVIJmi6k5qq9dLsYZfWLUxJJSg==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.758.0", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.693.0.tgz", + "integrity": "sha512-6EUfuKOujtddy18OLJUaXfKBgs+UcbZ6N/3QV4iOkubCUdeM1maIqs++B9bhCbWeaeF5ORizJw5FTwnyNjE/mw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/nested-clients": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/types": { - "version": "3.734.0", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.693.0.tgz", + "integrity": "sha512-td0OVX8m5ZKiXtecIDuzY3Y3UZIzvxEr57Hp21NOwieqKCG2UeyQWWeGPv0FQaU7dpTkvFmVNI+tx9iB8V/Nhg==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/core": { - "version": "3.1.5", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/middleware-serde": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-stream": "^4.1.2", - "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-middleware": "^4.0.1", + "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", + "node_modules/@aws-sdk/client-redshift-data/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-serverless": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-redshift-serverless/-/client-redshift-serverless-3.693.0.tgz", + "integrity": "sha512-m6Bhw0Xx/x0KGKP9N7c+Jqs5VT6nkZbfwO+QTxllggsuNfAzGwluCw1hoY++/MQ9oFtioEu+ud7xWOlTIK8w/A==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/client-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.693.0.tgz", + "integrity": "sha512-QEynrBC26x6TG9ZMzApR/kZ3lmt4lEIs2D+cHuDxt6fDGzahBUsQFBwJqhizzsM97JJI5YvmJhmihoYjdSSaXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/property-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.693.0.tgz", + "integrity": "sha512-UEDbYlYtK/e86OOMyFR4zEPyenIxDzO2DRdz3fwVW7RzZ94wfmSwBh/8skzPTuY1G7sI064cjHW0b0QG01Sdtg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.693.0.tgz", + "integrity": "sha512-4S2y7VEtvdnjJX4JPl4kDQlslxXEZFnC50/UXVUYSt/AMc5A/GgspFNA5FVz4E3Gwpfobbf23hR2NBF8AGvYoQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.693.0.tgz", + "integrity": "sha512-v6Z/kWmLFqRLDPEwl9hJGhtTgIFHjZugSfF1Yqffdxf4n1AWgtHS7qSegakuMyN5pP4K2tvUD8qHJ+gGe2Bw2A==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/signature-v4": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.693.0.tgz", + "integrity": "sha512-sL8MvwNJU7ZpD7/d2VVb3by1GknIJUxzTIgYtVkDVA/ojo+KRQSSHxcj0EWWXF5DTSh2Tm+LrEug3y1ZyKHsDA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/smithy-client": { - "version": "4.1.6", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.693.0.tgz", + "integrity": "sha512-kvaa4mXhCCOuW7UQnBhYqYfgWmwy7WSBSDClutwSLPZvgrhYj2l16SD2lN4IfYdxARYMJJ1lFYp3/jJG/9Yk4Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.6.2" + "node": ">=16.0.0" }, - "engines": { - "node": ">=18.0.0" + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/url-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.693.0.tgz", + "integrity": "sha512-42WMsBjTNnjYxYuM3qD/Nq+8b7UdMopUq5OduMDxoM3mFTV6PXMMnfI4Z1TNnR4tYRvPXAnuNltF6xmjKbSJRA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.693.0.tgz", + "integrity": "sha512-479UlJxY+BFjj3pJFYUNC0DCMrykuG7wBAXfsvZqQxKUa83DnH5Q1ID/N2hZLkxjGd4ZW0AC3lTOMxFelGzzpQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-middleware": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.693.0.tgz", + "integrity": "sha512-8LB210Pr6VeCiSb2hIra+sAH4KUBLyGaN50axHtIgufVK8jbKIctTZcVY5TO9Se+1107TsruzeXS7VeqVdJfFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "tslib": "^2.6.2" + "node": ">=16.0.0" }, - "engines": { - "node": ">=18.0.0" + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/middleware-host-header": { + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/middleware-host-header": { "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.693.0.tgz", + "integrity": "sha512-BCki6sAZ5jYwIN/t3ElCiwerHad69ipHwPsDCxJQyeiOnJ8HG+lEpnVIfrnI8A0fLQNSF3Gtx6ahfBpKiv1Oug==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.692.0", @@ -6000,8 +16090,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/middleware-logger": { "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.693.0.tgz", + "integrity": "sha512-dXnXDPr+wIiJ1TLADACI1g9pkSB21KkMIko2u4CJ2JCBoxi5IqeTnVoa6YcC8GdFNVRl+PorZ3Zqfmf1EOTC6w==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.692.0", @@ -6012,8 +16104,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.693.0.tgz", + "integrity": "sha512-0LDmM+VxXp0u3rG0xQRWD/q6Ubi7G8I44tBPahevD5CaiDZTkmNTrVUf0VEJgVe0iCKBppACMBDkLB0/ETqkFw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.692.0", @@ -6025,8 +16119,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/middleware-user-agent": { + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.693.0.tgz", + "integrity": "sha512-/KUq/KEpFFbQmNmpp7SpAtFAdViquDfD2W0QcG07zYBfz9MwE2ig48ALynXm5sMpRmnG7sJXjdvPtTsSVPfkiw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.693.0", @@ -6041,8 +16137,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/region-config-resolver": { + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/region-config-resolver": { "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.693.0.tgz", + "integrity": "sha512-YLUkMsUY0GLW/nfwlZ69cy1u07EZRmsv8Z9m0qW317/EZaVx59hcvmcvb+W4bFqj5E8YImTjoGfE4cZ0F9mkyw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.692.0", @@ -6056,8 +16154,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/token-providers": { + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/token-providers": { "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.693.0.tgz", + "integrity": "sha512-nDBTJMk1l/YmFULGfRbToOA2wjf+FkQT4dMgYCv+V9uSYsMzQj8A7Tha2dz9yv4vnQgYaEiErQ8d7HVyXcVEoA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.692.0", @@ -6073,8 +16173,10 @@ "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/util-endpoints": { + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/util-endpoints": { "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.693.0.tgz", + "integrity": "sha512-eo4F6DRQ/kxS3gxJpLRv+aDNy76DxQJL5B3DPzpr9Vkq0ygVoi4GT5oIZLVaAVIJmi6k5qq9dLsYZfWLUxJJSg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.692.0", @@ -6086,8 +16188,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/util-user-agent-browser": { + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.693.0.tgz", + "integrity": "sha512-6EUfuKOujtddy18OLJUaXfKBgs+UcbZ6N/3QV4iOkubCUdeM1maIqs++B9bhCbWeaeF5ORizJw5FTwnyNjE/mw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.692.0", @@ -6096,8 +16200,10 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/util-user-agent-node": { + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.693.0.tgz", + "integrity": "sha512-td0OVX8m5ZKiXtecIDuzY3Y3UZIzvxEr57Hp21NOwieqKCG2UeyQWWeGPv0FQaU7dpTkvFmVNI+tx9iB8V/Nhg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/middleware-user-agent": "3.693.0", @@ -6118,777 +16224,787 @@ } } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/abort-controller": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/abort-controller/node_modules/@smithy/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds/node_modules/@smithy/property-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds/node_modules/@smithy/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/credential-provider-imds/node_modules/@smithy/url-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/querystring-builder": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/types": "^4.1.0", - "@smithy/util-uri-escape": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/querystring-builder/node_modules/@smithy/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/querystring-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/querystring-parser/node_modules/@smithy/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/service-error-classification": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/types": "^4.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/service-error-classification/node_modules/@smithy/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream": { - "version": "4.1.2", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-redshift-serverless/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/client-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.693.0.tgz", + "integrity": "sha512-QEynrBC26x6TG9ZMzApR/kZ3lmt4lEIs2D+cHuDxt6fDGzahBUsQFBwJqhizzsM97JJI5YvmJhmihoYjdSSaXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/abort-controller": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.693.0.tgz", + "integrity": "sha512-UEDbYlYtK/e86OOMyFR4zEPyenIxDzO2DRdz3fwVW7RzZ94wfmSwBh/8skzPTuY1G7sI064cjHW0b0QG01Sdtg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.6.2" + "node": ">=16.0.0" }, - "engines": { - "node": ">=18.0.0" + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/util-base64": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.693.0.tgz", + "integrity": "sha512-4S2y7VEtvdnjJX4JPl4kDQlslxXEZFnC50/UXVUYSt/AMc5A/GgspFNA5FVz4E3Gwpfobbf23hR2NBF8AGvYoQ==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/util-utf8": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.693.0.tgz", + "integrity": "sha512-v6Z/kWmLFqRLDPEwl9hJGhtTgIFHjZugSfF1Yqffdxf4n1AWgtHS7qSegakuMyN5pP4K2tvUD8qHJ+gGe2Bw2A==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.693.0.tgz", + "integrity": "sha512-sL8MvwNJU7ZpD7/d2VVb3by1GknIJUxzTIgYtVkDVA/ojo+KRQSSHxcj0EWWXF5DTSh2Tm+LrEug3y1ZyKHsDA==", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-utf8": { - "version": "3.0.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.693.0.tgz", + "integrity": "sha512-kvaa4mXhCCOuW7UQnBhYqYfgWmwy7WSBSDClutwSLPZvgrhYj2l16SD2lN4IfYdxARYMJJ1lFYp3/jJG/9Yk4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-utf8/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.693.0.tgz", + "integrity": "sha512-42WMsBjTNnjYxYuM3qD/Nq+8b7UdMopUq5OduMDxoM3mFTV6PXMMnfI4Z1TNnR4tYRvPXAnuNltF6xmjKbSJRA==", "license": "Apache-2.0", "dependencies": { + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.693.0.tgz", + "integrity": "sha512-479UlJxY+BFjj3pJFYUNC0DCMrykuG7wBAXfsvZqQxKUa83DnH5Q1ID/N2hZLkxjGd4ZW0AC3lTOMxFelGzzpQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.730.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.693.0.tgz", + "integrity": "sha512-8LB210Pr6VeCiSb2hIra+sAH4KUBLyGaN50axHtIgufVK8jbKIctTZcVY5TO9Se+1107TsruzeXS7VeqVdJfFA==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.730.0", - "@aws-sdk/credential-provider-node": "3.730.0", - "@aws-sdk/middleware-host-header": "3.723.0", - "@aws-sdk/middleware-logger": "3.723.0", - "@aws-sdk/middleware-recursion-detection": "3.723.0", - "@aws-sdk/middleware-user-agent": "3.730.0", - "@aws-sdk/region-config-resolver": "3.723.0", - "@aws-sdk/types": "3.723.0", - "@aws-sdk/util-endpoints": "3.730.0", - "@aws-sdk/util-user-agent-browser": "3.723.0", - "@aws-sdk/util-user-agent-node": "3.730.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", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/client-sso": { - "version": "3.730.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.693.0.tgz", + "integrity": "sha512-BCki6sAZ5jYwIN/t3ElCiwerHad69ipHwPsDCxJQyeiOnJ8HG+lEpnVIfrnI8A0fLQNSF3Gtx6ahfBpKiv1Oug==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.730.0", - "@aws-sdk/middleware-host-header": "3.723.0", - "@aws-sdk/middleware-logger": "3.723.0", - "@aws-sdk/middleware-recursion-detection": "3.723.0", - "@aws-sdk/middleware-user-agent": "3.730.0", - "@aws-sdk/region-config-resolver": "3.723.0", - "@aws-sdk/types": "3.723.0", - "@aws-sdk/util-endpoints": "3.730.0", - "@aws-sdk/util-user-agent-browser": "3.723.0", - "@aws-sdk/util-user-agent-node": "3.730.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", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/core": { - "version": "3.730.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.693.0.tgz", + "integrity": "sha512-dXnXDPr+wIiJ1TLADACI1g9pkSB21KkMIko2u4CJ2JCBoxi5IqeTnVoa6YcC8GdFNVRl+PorZ3Zqfmf1EOTC6w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.723.0", - "@smithy/core": "^3.0.0", - "@smithy/node-config-provider": "^4.0.0", - "@smithy/property-provider": "^4.0.0", - "@smithy/protocol-http": "^5.0.0", - "@smithy/signature-v4": "^5.0.0", - "@smithy/smithy-client": "^4.0.0", - "@smithy/types": "^4.0.0", - "@smithy/util-middleware": "^4.0.0", - "fast-xml-parser": "4.4.1", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.730.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.693.0.tgz", + "integrity": "sha512-0LDmM+VxXp0u3rG0xQRWD/q6Ubi7G8I44tBPahevD5CaiDZTkmNTrVUf0VEJgVe0iCKBppACMBDkLB0/ETqkFw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.730.0", - "@aws-sdk/types": "3.723.0", - "@smithy/property-provider": "^4.0.0", - "@smithy/types": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.730.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.693.0.tgz", + "integrity": "sha512-/KUq/KEpFFbQmNmpp7SpAtFAdViquDfD2W0QcG07zYBfz9MwE2ig48ALynXm5sMpRmnG7sJXjdvPtTsSVPfkiw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.730.0", - "@aws-sdk/types": "3.723.0", - "@smithy/fetch-http-handler": "^5.0.0", - "@smithy/node-http-handler": "^4.0.0", - "@smithy/property-provider": "^4.0.0", - "@smithy/protocol-http": "^5.0.0", - "@smithy/smithy-client": "^4.0.0", - "@smithy/types": "^4.0.0", - "@smithy/util-stream": "^4.0.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.730.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.693.0.tgz", + "integrity": "sha512-YLUkMsUY0GLW/nfwlZ69cy1u07EZRmsv8Z9m0qW317/EZaVx59hcvmcvb+W4bFqj5E8YImTjoGfE4cZ0F9mkyw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.730.0", - "@aws-sdk/credential-provider-env": "3.730.0", - "@aws-sdk/credential-provider-http": "3.730.0", - "@aws-sdk/credential-provider-process": "3.730.0", - "@aws-sdk/credential-provider-sso": "3.730.0", - "@aws-sdk/credential-provider-web-identity": "3.730.0", - "@aws-sdk/nested-clients": "3.730.0", - "@aws-sdk/types": "3.723.0", - "@smithy/credential-provider-imds": "^4.0.0", - "@smithy/property-provider": "^4.0.0", - "@smithy/shared-ini-file-loader": "^4.0.0", - "@smithy/types": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.730.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.693.0.tgz", + "integrity": "sha512-nDBTJMk1l/YmFULGfRbToOA2wjf+FkQT4dMgYCv+V9uSYsMzQj8A7Tha2dz9yv4vnQgYaEiErQ8d7HVyXcVEoA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.730.0", - "@aws-sdk/credential-provider-http": "3.730.0", - "@aws-sdk/credential-provider-ini": "3.730.0", - "@aws-sdk/credential-provider-process": "3.730.0", - "@aws-sdk/credential-provider-sso": "3.730.0", - "@aws-sdk/credential-provider-web-identity": "3.730.0", - "@aws-sdk/types": "3.723.0", - "@smithy/credential-provider-imds": "^4.0.0", - "@smithy/property-provider": "^4.0.0", - "@smithy/shared-ini-file-loader": "^4.0.0", - "@smithy/types": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.730.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.693.0.tgz", + "integrity": "sha512-eo4F6DRQ/kxS3gxJpLRv+aDNy76DxQJL5B3DPzpr9Vkq0ygVoi4GT5oIZLVaAVIJmi6k5qq9dLsYZfWLUxJJSg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.730.0", - "@aws-sdk/types": "3.723.0", - "@smithy/property-provider": "^4.0.0", - "@smithy/shared-ini-file-loader": "^4.0.0", - "@smithy/types": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.730.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.693.0.tgz", + "integrity": "sha512-6EUfuKOujtddy18OLJUaXfKBgs+UcbZ6N/3QV4iOkubCUdeM1maIqs++B9bhCbWeaeF5ORizJw5FTwnyNjE/mw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.730.0", - "@aws-sdk/core": "3.730.0", - "@aws-sdk/token-providers": "3.730.0", - "@aws-sdk/types": "3.723.0", - "@smithy/property-provider": "^4.0.0", - "@smithy/shared-ini-file-loader": "^4.0.0", - "@smithy/types": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.730.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.693.0.tgz", + "integrity": "sha512-td0OVX8m5ZKiXtecIDuzY3Y3UZIzvxEr57Hp21NOwieqKCG2UeyQWWeGPv0FQaU7dpTkvFmVNI+tx9iB8V/Nhg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.730.0", - "@aws-sdk/nested-clients": "3.730.0", - "@aws-sdk/types": "3.723.0", - "@smithy/property-provider": "^4.0.0", - "@smithy/types": "^4.0.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.723.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.723.0", - "@smithy/protocol-http": "^5.0.0", - "@smithy/types": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-logger": { - "version": "3.723.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.723.0", - "@smithy/types": "^4.0.0", + "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.723.0", + "node_modules/@aws-sdk/client-redshift/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.723.0", - "@smithy/protocol-http": "^5.0.0", - "@smithy/types": "^4.0.0", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.730.0", + "node_modules/@aws-sdk/client-s3": { + "version": "3.693.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.730.0", - "@aws-sdk/types": "3.723.0", - "@aws-sdk/util-endpoints": "3.730.0", - "@smithy/core": "^3.0.0", - "@smithy/protocol-http": "^5.0.0", - "@smithy/types": "^4.0.0", + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-bucket-endpoint": "3.693.0", + "@aws-sdk/middleware-expect-continue": "3.693.0", + "@aws-sdk/middleware-flexible-checksums": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-location-constraint": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-sdk-s3": "3.693.0", + "@aws-sdk/middleware-ssec": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/signature-v4-multi-region": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@aws-sdk/xml-builder": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/eventstream-serde-browser": "^3.0.12", + "@smithy/eventstream-serde-config-resolver": "^3.0.9", + "@smithy/eventstream-serde-node": "^3.0.11", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-blob-browser": "^3.1.8", + "@smithy/hash-node": "^3.0.9", + "@smithy/hash-stream-node": "^3.1.8", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/md5-js": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-stream": "^3.3.0", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/nested-clients": { - "version": "3.730.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control": { + "version": "3.859.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3-control/-/client-s3-control-3.859.0.tgz", + "integrity": "sha512-vzhOtDH4BCdn30+Crg1QxGXbhZIh4Ia84/qNx2EtupkM2UrO6uaZ91qGl175QWU4TcG+mlf/yA/bvrwenhbF6w==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.730.0", - "@aws-sdk/middleware-host-header": "3.723.0", - "@aws-sdk/middleware-logger": "3.723.0", - "@aws-sdk/middleware-recursion-detection": "3.723.0", - "@aws-sdk/middleware-user-agent": "3.730.0", - "@aws-sdk/region-config-resolver": "3.723.0", - "@aws-sdk/types": "3.723.0", - "@aws-sdk/util-endpoints": "3.730.0", - "@aws-sdk/util-user-agent-browser": "3.723.0", - "@aws-sdk/util-user-agent-node": "3.730.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", + "@aws-sdk/core": "3.858.0", + "@aws-sdk/credential-provider-node": "3.859.0", + "@aws-sdk/middleware-bucket-endpoint": "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-sdk-s3-control": "3.848.0", + "@aws-sdk/middleware-user-agent": "3.858.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.858.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.2", + "@smithy/fetch-http-handler": "^5.1.0", + "@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-apply-body-checksum": "^4.1.2", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.17", + "@smithy/middleware-retry": "^4.1.18", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.9", + "@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.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-defaults-mode-browser": "^4.0.25", + "@smithy/util-defaults-mode-node": "^4.0.25", + "@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" + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.723.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/client-sso": { + "version": "3.858.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.858.0.tgz", + "integrity": "sha512-iXuZQs4KH6a3Pwnt0uORalzAZ5EXRPr3lBYAsdNwkP8OYyoUz5/TE3BLyw7ceEh0rj4QKGNnNALYo1cDm0EV8w==", "dependencies": { - "@aws-sdk/types": "3.723.0", - "@smithy/node-config-provider": "^4.0.0", - "@smithy/types": "^4.0.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.858.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.858.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.858.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.2", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.17", + "@smithy/middleware-retry": "^4.1.18", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.9", + "@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.25", + "@smithy/util-defaults-mode-node": "^4.0.25", + "@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-cognito-identity/node_modules/@aws-sdk/token-providers": { - "version": "3.730.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/core": { + "version": "3.858.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.858.0.tgz", + "integrity": "sha512-iWm4QLAS+/XMlnecIU1Y33qbBr1Ju+pmWam3xVCPlY4CSptKpVY+2hXOnmg9SbHAX9C005fWhrIn51oDd00c9A==", "dependencies": { - "@aws-sdk/nested-clients": "3.730.0", - "@aws-sdk/types": "3.723.0", - "@smithy/property-provider": "^4.0.0", - "@smithy/shared-ini-file-loader": "^4.0.0", - "@smithy/types": "^4.0.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.7.2", + "@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.9", + "@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": "5.2.5", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/types": { - "version": "3.723.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.858.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.858.0.tgz", + "integrity": "sha512-kZsGyh2BoSRguzlcGtzdLhw/l/n3KYAC+/l/H0SlsOq3RLHF6tO/cRdsLnwoix2bObChHUp03cex63o1gzdx/Q==", "dependencies": { - "@smithy/types": "^4.0.0", + "@aws-sdk/core": "3.858.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-cognito-identity/node_modules/@aws-sdk/util-endpoints": { - "version": "3.730.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.858.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.858.0.tgz", + "integrity": "sha512-GDnfYl3+NPJQ7WQQYOXEA489B212NinpcIDD7rpsB6IWUPo8yDjT5NceK4uUkIR3MFpNCGt9zd/z6NNLdB2fuQ==", "dependencies": { - "@aws-sdk/types": "3.723.0", - "@smithy/types": "^4.0.0", - "@smithy/util-endpoints": "^3.0.0", + "@aws-sdk/core": "3.858.0", + "@aws-sdk/types": "3.840.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.9", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.3", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.723.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.723.0", - "@smithy/types": "^4.0.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.730.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.859.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.859.0.tgz", + "integrity": "sha512-KsccE1T88ZDNhsABnqbQj014n5JMDilAroUErFbGqu5/B3sXqUsYmG54C/BjvGTRUFfzyttK9lB9P9h6ddQ8Cw==", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.730.0", - "@aws-sdk/types": "3.723.0", - "@smithy/node-config-provider": "^4.0.0", - "@smithy/types": "^4.0.0", + "@aws-sdk/core": "3.858.0", + "@aws-sdk/credential-provider-env": "3.858.0", + "@aws-sdk/credential-provider-http": "3.858.0", + "@aws-sdk/credential-provider-process": "3.858.0", + "@aws-sdk/credential-provider-sso": "3.859.0", + "@aws-sdk/credential-provider-web-identity": "3.858.0", + "@aws-sdk/nested-clients": "3.858.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" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/abort-controller": { - "version": "4.0.4", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.859.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.859.0.tgz", + "integrity": "sha512-ZRDB2xU5aSyTR/jDcli30tlycu6RFvQngkZhBs9Zoh2BiYXrfh2MMuoYuZk+7uD6D53Q2RIEldDHR9A/TPlRuA==", "dependencies": { + "@aws-sdk/credential-provider-env": "3.858.0", + "@aws-sdk/credential-provider-http": "3.858.0", + "@aws-sdk/credential-provider-ini": "3.859.0", + "@aws-sdk/credential-provider-process": "3.858.0", + "@aws-sdk/credential-provider-sso": "3.859.0", + "@aws-sdk/credential-provider-web-identity": "3.858.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" }, @@ -6896,83 +17012,93 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/config-resolver": { - "version": "4.1.4", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.858.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.858.0.tgz", + "integrity": "sha512-l5LJWZJMRaZ+LhDjtupFUKEC5hAjgvCRrOvV5T60NCUBOy0Ozxa7Sgx3x+EOwiruuoh3Cn9O+RlbQlJX6IfZIw==", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", + "@aws-sdk/core": "3.858.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", - "@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-cognito-identity/node_modules/@smithy/core": { - "version": "3.5.3", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.859.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.859.0.tgz", + "integrity": "sha512-BwAqmWIivhox5YlFRjManFF8GoTvEySPk6vsJNxDsmGsabY+OQovYxFIYxRCYiHzH7SFjd4Lcd+riJOiXNsvRw==", "dependencies": { - "@smithy/middleware-serde": "^4.0.8", - "@smithy/protocol-http": "^5.1.2", + "@aws-sdk/client-sso": "3.858.0", + "@aws-sdk/core": "3.858.0", + "@aws-sdk/token-providers": "3.859.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", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-stream": "^4.2.2", - "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/credential-provider-imds": { - "version": "4.0.6", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.858.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.858.0.tgz", + "integrity": "sha512-8iULWsH83iZDdUuiDsRb83M0NqIlXjlDbJUIddVsIrfWp4NmanKw77SV6yOZ66nuJjPsn9j7RDb9bfEPCy5SWA==", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", + "@aws-sdk/core": "3.858.0", + "@aws-sdk/nested-clients": "3.858.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.4", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.840.0.tgz", + "integrity": "sha512-+gkQNtPwcSMmlwBHFd4saVVS11In6ID1HczNzpM3MXKXRBfSlbZJbCt6wN//AZ8HMklZEik4tcEOG0qa9UY8SQ==", "dependencies": { + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/node-config-provider": "^4.1.3", "@smithy/protocol-http": "^5.1.2", - "@smithy/querystring-builder": "^4.0.4", "@smithy/types": "^4.3.1", - "@smithy/util-base64": "^4.0.0", + "@smithy/util-config-provider": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/hash-node": { - "version": "4.0.4", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/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==", "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/invalid-dependency": { - "version": "4.0.4", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/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==", "dependencies": { + "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, @@ -6980,20 +17106,29 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/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==", "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-cognito-identity/node_modules/@smithy/middleware-content-length": { - "version": "4.0.4", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.858.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.858.0.tgz", + "integrity": "sha512-pC3FT/sRZ6n5NyXiTVu9dpf1D9j3YbJz3XmeOOwJqO/Mib2PZyIQktvNMPgwaC5KMVB1zWqS5bmCwxpMOnq0UQ==", "dependencies": { + "@aws-sdk/core": "3.858.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@smithy/core": "^3.7.2", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -7002,46 +17137,80 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-endpoint": { - "version": "4.1.11", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/nested-clients": { + "version": "3.858.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.858.0.tgz", + "integrity": "sha512-ChdIj80T2whoWbovmO7o8ICmhEB2S9q4Jes9MBnKAPm69PexcJAK2dQC8yI4/iUP8b3+BHZoUPrYLWjBxIProQ==", "dependencies": { - "@smithy/core": "^3.5.3", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.858.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.858.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.858.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.2", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.17", + "@smithy/middleware-retry": "^4.1.18", "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", "@smithy/node-config-provider": "^4.1.3", - "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.9", "@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.25", + "@smithy/util-defaults-mode-node": "^4.0.25", + "@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-cognito-identity/node_modules/@smithy/middleware-retry": { - "version": "4.1.12", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/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==", "dependencies": { + "@aws-sdk/types": "3.840.0", "@smithy/node-config-provider": "^4.1.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/service-error-classification": "^4.0.5", - "@smithy/smithy-client": "^4.4.3", "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.4", - "@smithy/util-retry": "^4.0.5", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-serde": { - "version": "4.0.8", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/token-providers": { + "version": "3.859.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.859.0.tgz", + "integrity": "sha512-6P2wlvm9KBWOvRNn0Pt8RntnXg8fzOb5kEShvWsOsAocZeqKNaYbihum5/Onq1ZPoVtkdb++8eWDocDnM4k85Q==", "dependencies": { - "@smithy/protocol-http": "^5.1.2", + "@aws-sdk/core": "3.858.0", + "@aws-sdk/nested-clients": "3.858.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" }, @@ -7049,9 +17218,10 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-stack": { - "version": "4.0.4", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/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==", "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -7060,47 +17230,70 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/node-config-provider": { - "version": "4.1.3", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", + "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", "dependencies": { - "@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-cognito-identity/node_modules/@smithy/node-http-handler": { - "version": "4.0.6", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/util-endpoints": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.848.0.tgz", + "integrity": "sha512-fY/NuFFCq/78liHvRyFKr+aqq1aA/uuVSANjzr5Ym8c+9Z3HRPE9OrExAHoMrZ6zC8tHerQwlsXYYH5XZ7H+ww==", "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/url-parser": "^4.0.4", + "@smithy/util-endpoints": "^3.0.6", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/property-provider": { - "version": "4.0.4", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/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==", + "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-s3-control/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.858.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.858.0.tgz", + "integrity": "sha512-T1m05QlN8hFpx5/5duMjS8uFSK5e6EXP45HQRkZULVkL3DK+jMaxsnh3KLl5LjUoHn/19M4HM0wNUBhYp4Y2Yw==", "dependencies": { + "@aws-sdk/middleware-user-agent": "3.858.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-cognito-identity/node_modules/@smithy/protocol-http": { - "version": "5.1.2", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/xml-builder": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", + "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -7109,60 +17302,68 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/querystring-builder": { + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/abort-controller": { "version": "4.0.4", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", "dependencies": { "@smithy/types": "^4.3.1", - "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/querystring-parser": { - "version": "4.0.4", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/chunked-blob-reader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz", + "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", "dependencies": { - "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/service-error-classification": { - "version": "4.0.5", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", + "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", "dependencies": { - "@smithy/types": "^4.3.1" + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.4", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/config-resolver": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", + "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", "dependencies": { + "@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-cognito-identity/node_modules/@smithy/signature-v4": { - "version": "5.1.2", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/core": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.7.2.tgz", + "integrity": "sha512-JoLw59sT5Bm8SAjFCYZyuCGxK8y3vovmoVbZWLDPTH5XpPEIwpFd9m90jjVMwoypDuB/SdVgje5Y4T7w50lJaw==", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", + "@smithy/middleware-serde": "^4.0.8", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", - "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.4", - "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-stream": "^4.2.3", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -7170,37 +17371,43 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/smithy-client": { - "version": "4.4.3", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/credential-provider-imds": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", + "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", "dependencies": { - "@smithy/core": "^3.5.3", - "@smithy/middleware-endpoint": "^4.1.11", - "@smithy/middleware-stack": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", "@smithy/types": "^4.3.1", - "@smithy/util-stream": "^4.2.2", + "@smithy/url-parser": "^4.0.4", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/types": { - "version": "4.3.1", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/fetch-http-handler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.0.tgz", + "integrity": "sha512-mADw7MS0bYe2OGKkHYMaqarOXuDwRbO6ArD91XhHcl2ynjGCFF+hvqf0LyQcYxkA1zaWjefSkU7Ne9mqgApSgQ==", "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/url-parser": { + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/hash-blob-browser": { "version": "4.0.4", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.4.tgz", + "integrity": "sha512-WszRiACJiQV3QG6XMV44i5YWlkrlsM5Yxgz4jvsksuu7LDXA6wAtypfPajtNTadzpJy3KyJPoWehYpmZGKUFIQ==", "dependencies": { - "@smithy/querystring-parser": "^4.0.4", + "@smithy/chunked-blob-reader": "^5.0.0", + "@smithy/chunked-blob-reader-native": "^4.0.0", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, @@ -7208,10 +17415,12 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-base64": { - "version": "4.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/hash-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", + "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", "dependencies": { + "@smithy/types": "^4.3.1", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" @@ -7220,102 +17429,122 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/hash-stream-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.4.tgz", + "integrity": "sha512-wHo0d8GXyVmpmMh/qOR0R7Y46/G1y6OR8U+bSTB4ppEzRxd1xVAQ9xOE9hOc0bSjhz0ujCPAbfNLkLrpa6cevg==", "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/invalid-dependency": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", + "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", "dependencies": { + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/is-array-buffer": { "version": "4.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-config-provider": { - "version": "4.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/md5-js": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.4.tgz", + "integrity": "sha512-uGLBVqcOwrLvGh/v/jw423yWHq/ofUGK1W31M2TNspLQbUV1Va0F5kTxtirkoHawODAZcjXTSGi7JwbnPcDPJg==", "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.19", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/middleware-content-length": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", + "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", "dependencies": { - "@smithy/property-provider": "^4.0.4", - "@smithy/smithy-client": "^4.4.3", + "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", - "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.19", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/middleware-endpoint": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.17.tgz", + "integrity": "sha512-S3hSGLKmHG1m35p/MObQCBCdRsrpbPU8B129BVzRqRfDvQqPMQ14iO4LyRw+7LNizYc605COYAcjqgawqi+6jA==", "dependencies": { - "@smithy/config-resolver": "^4.1.4", - "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/core": "^3.7.2", + "@smithy/middleware-serde": "^4.0.8", "@smithy/node-config-provider": "^4.1.3", - "@smithy/property-provider": "^4.0.4", - "@smithy/smithy-client": "^4.4.3", + "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-endpoints": { - "version": "3.0.6", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/middleware-retry": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.18.tgz", + "integrity": "sha512-bYLZ4DkoxSsPxpdmeapvAKy7rM5+25gR7PGxq2iMiecmbrRGBHj9s75N74Ylg+aBiw9i5jIowC/cLU2NR0qH8w==", "dependencies": { "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/smithy-client": "^4.4.9", "@smithy/types": "^4.3.1", - "tslib": "^2.6.2" + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/middleware-serde": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", + "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", "dependencies": { + "@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-cognito-identity/node_modules/@smithy/util-middleware": { + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/middleware-stack": { "version": "4.0.4", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", + "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -7324,11 +17553,13 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-retry": { - "version": "4.0.5", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/node-config-provider": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", + "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", "dependencies": { - "@smithy/service-error-classification": "^4.0.5", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, @@ -7336,578 +17567,361 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-stream": { - "version": "4.2.2", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/node-http-handler": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.0.tgz", + "integrity": "sha512-vqfSiHz2v8b3TTTrdXi03vNz1KLYYS3bhHCDv36FYDqxT7jvTll1mMnCrkD+gOvgwybuunh/2VmvOMqwBegxEg==", "dependencies": { - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/node-http-handler": "^4.0.6", + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", "@smithy/types": "^4.3.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/property-provider": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", + "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", "dependencies": { + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", + "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2": { - "version": "3.695.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/querystring-builder": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", + "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.693.0", - "@aws-sdk/client-sts": "3.693.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-sdk-ec2": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.1.8", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/types": "^4.3.1", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/client-sso": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/service-error-classification": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.6.tgz", + "integrity": "sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" + "@smithy/types": "^4.3.1" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/client-sts": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", + "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.693.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/core": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/signature-v4": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", + "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/core": "^2.5.2", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/property-provider": "^3.1.9", - "@smithy/protocol-http": "^4.1.6", - "@smithy/signature-v4": "^4.2.2", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/util-middleware": "^3.0.9", - "fast-xml-parser": "4.4.1", + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/smithy-client": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.9.tgz", + "integrity": "sha512-mbMg8mIUAWwMmb74LoYiArP04zWElPzDoA1jVOp3or0cjlDMgoS6WTC3QXK0Vxoc9I4zdrX0tq6qsOmaIoTWEQ==", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/util-stream": "^3.3.0", + "@smithy/core": "^3.7.2", + "@smithy/middleware-endpoint": "^4.1.17", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.3", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/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": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-env": "3.693.0", - "@aws-sdk/credential-provider-http": "3.693.0", - "@aws-sdk/credential-provider-process": "3.693.0", - "@aws-sdk/credential-provider-sso": "3.693.0", - "@aws-sdk/credential-provider-web-identity": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/credential-provider-imds": "^3.2.6", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.693.0", - "@aws-sdk/credential-provider-http": "3.693.0", - "@aws-sdk/credential-provider-ini": "3.693.0", - "@aws-sdk/credential-provider-process": "3.693.0", - "@aws-sdk/credential-provider-sso": "3.693.0", - "@aws-sdk/credential-provider-web-identity": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/credential-provider-imds": "^3.2.6", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", "dependencies": { - "@aws-sdk/client-sso": "3.693.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/token-providers": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-logger": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", + "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.25.tgz", + "integrity": "sha512-pxEWsxIsOPLfKNXvpgFHBGFC3pKYKUFhrud1kyooO9CJai6aaKDHfT10Mi5iiipPXN/JhKAu3qX9o75+X85OdQ==", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@smithy/core": "^2.5.2", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.9", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.25.tgz", + "integrity": "sha512-+w4n4hKFayeCyELZLfsSQG5mCC3TwSkmRHv4+el5CzFU8ToQpYGhpV7mrRzqlwKkntlPilT1HJy1TVeEvEjWOQ==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/types": "^3.7.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.9", + "@smithy/config-resolver": "^4.1.4", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.9", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/token-providers": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", + "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/util-endpoints": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "@smithy/util-endpoints": "^2.1.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", + "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "bowser": "^2.11.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-retry": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.6.tgz", + "integrity": "sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-stream": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.3.tgz", + "integrity": "sha512-cQn412DWHHFNKrQfbHY8vSFI3nTROY1aIKji9N0tpp8gUABRilr7wdf8fqBbSlXresobM+tQFNk6I+0LXK/YZg==", "dependencies": { + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@smithy/util-utf8": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-iam": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-s3-control/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.693.0", - "@aws-sdk/client-sts": "3.693.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.1.8", - "tslib": "^2.6.2" + "strnum": "^2.1.0" }, - "engines": { - "node": ">=16.0.0" + "bin": { + "fxparser": "src/cli/cli.js" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso": { + "node_modules/@aws-sdk/client-s3-control/node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -7954,9 +17968,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso-oidc": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso-oidc": { "version": "3.693.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -8005,9 +18020,10 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sts": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sts": { "version": "3.693.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -8054,7 +18070,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/core": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8074,7 +18090,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-http": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8093,7 +18109,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-ini": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8117,7 +18133,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-node": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8138,7 +18154,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-sso": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8155,7 +18171,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-web-identity": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8172,7 +18188,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-host-header": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8185,7 +18201,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8197,7 +18213,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8210,7 +18226,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-user-agent": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8226,7 +18242,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/region-config-resolver": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8241,7 +18257,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/token-providers": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8258,7 +18274,7 @@ "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-endpoints": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8271,7 +18287,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-browser": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8281,7 +18297,7 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-node": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8303,7 +18319,7 @@ } } }, - "node_modules/@aws-sdk/client-iam/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -8313,7 +18329,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-buffer-from": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -8324,105 +18340,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/@smithy/util-utf8": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-lambda": { - "version": "3.637.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.637.0", - "@aws-sdk/client-sts": "3.637.0", - "@aws-sdk/core": "3.635.0", - "@aws-sdk/credential-provider-node": "3.637.0", - "@aws-sdk/middleware-host-header": "3.620.0", - "@aws-sdk/middleware-logger": "3.609.0", - "@aws-sdk/middleware-recursion-detection": "3.620.0", - "@aws-sdk/middleware-user-agent": "3.637.0", - "@aws-sdk/region-config-resolver": "3.614.0", - "@aws-sdk/types": "3.609.0", - "@aws-sdk/util-endpoints": "3.637.0", - "@aws-sdk/util-user-agent-browser": "3.609.0", - "@aws-sdk/util-user-agent-node": "3.614.0", - "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.4.0", - "@smithy/eventstream-serde-browser": "^3.0.6", - "@smithy/eventstream-serde-config-resolver": "^3.0.3", - "@smithy/eventstream-serde-node": "^3.0.5", - "@smithy/fetch-http-handler": "^3.2.4", - "@smithy/hash-node": "^3.0.3", - "@smithy/invalid-dependency": "^3.0.3", - "@smithy/middleware-content-length": "^3.0.5", - "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", - "@smithy/middleware-serde": "^3.0.3", - "@smithy/middleware-stack": "^3.0.3", - "@smithy/node-config-provider": "^3.1.4", - "@smithy/node-http-handler": "^3.1.4", - "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", - "@smithy/types": "^3.3.0", - "@smithy/url-parser": "^3.0.3", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.15", - "@smithy/util-defaults-mode-node": "^3.0.15", - "@smithy/util-endpoints": "^2.0.5", - "@smithy/util-middleware": "^3.0.3", - "@smithy/util-retry": "^3.0.3", - "@smithy/util-stream": "^3.1.3", - "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.1.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/types": { - "version": "3.609.0", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^3.3.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.4", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^4.1.0", - "@smithy/querystring-builder": "^3.0.3", - "@smithy/types": "^3.3.0", - "@smithy/util-base64": "^3.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-utf8": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -8433,56 +18351,30 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3": { + "node_modules/@aws-sdk/client-sagemaker": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/client-sso-oidc": "3.693.0", "@aws-sdk/client-sts": "3.693.0", "@aws-sdk/core": "3.693.0", "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-bucket-endpoint": "3.693.0", - "@aws-sdk/middleware-expect-continue": "3.693.0", - "@aws-sdk/middleware-flexible-checksums": "3.693.0", "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-location-constraint": "3.693.0", "@aws-sdk/middleware-logger": "3.693.0", "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-sdk-s3": "3.693.0", - "@aws-sdk/middleware-ssec": "3.693.0", "@aws-sdk/middleware-user-agent": "3.693.0", "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/signature-v4-multi-region": "3.693.0", "@aws-sdk/types": "3.692.0", "@aws-sdk/util-endpoints": "3.693.0", "@aws-sdk/util-user-agent-browser": "3.693.0", "@aws-sdk/util-user-agent-node": "3.693.0", - "@aws-sdk/xml-builder": "3.693.0", "@smithy/config-resolver": "^3.0.11", "@smithy/core": "^2.5.2", - "@smithy/eventstream-serde-browser": "^3.0.12", - "@smithy/eventstream-serde-config-resolver": "^3.0.9", - "@smithy/eventstream-serde-node": "^3.0.11", "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-blob-browser": "^3.1.8", "@smithy/hash-node": "^3.0.9", - "@smithy/hash-stream-node": "^3.1.8", "@smithy/invalid-dependency": "^3.0.9", - "@smithy/md5-js": "^3.0.9", "@smithy/middleware-content-length": "^3.0.11", "@smithy/middleware-endpoint": "^3.2.2", "@smithy/middleware-retry": "^3.0.26", @@ -8502,16 +18394,17 @@ "@smithy/util-endpoints": "^2.1.5", "@smithy/util-middleware": "^3.0.9", "@smithy/util-retry": "^3.0.9", - "@smithy/util-stream": "^3.3.0", "@smithy/util-utf8": "^3.0.0", "@smithy/util-waiter": "^3.1.8", - "tslib": "^2.6.2" + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/client-sso": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8558,9 +18451,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso-oidc": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/client-sso-oidc": { "version": "3.693.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -8609,9 +18503,10 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sts": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/client-sts": { "version": "3.693.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -8658,7 +18553,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/core": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8678,7 +18573,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/credential-provider-http": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8697,7 +18592,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/credential-provider-ini": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8721,7 +18616,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/credential-provider-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8742,7 +18637,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/credential-provider-sso": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8759,7 +18654,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8776,7 +18671,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/middleware-host-header": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8789,7 +18684,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/middleware-logger": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8801,7 +18696,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8814,7 +18709,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8830,7 +18725,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/region-config-resolver": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8845,7 +18740,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/token-providers": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8862,7 +18757,7 @@ "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/util-endpoints": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8875,7 +18770,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8885,7 +18780,7 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8907,7 +18802,7 @@ } } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -8917,7 +18812,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@smithy/util-buffer-from": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -8928,7 +18823,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-sagemaker/node_modules/@smithy/util-utf8": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -8939,7 +18834,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker": { + "node_modules/@aws-sdk/client-sfn": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -8983,7 +18878,6 @@ "@smithy/util-middleware": "^3.0.9", "@smithy/util-retry": "^3.0.9", "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.1.8", "@types/uuid": "^9.0.1", "tslib": "^2.6.2", "uuid": "^9.0.1" @@ -8992,7 +18886,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/client-sso": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/client-sso": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9039,9 +18933,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/client-sso-oidc": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/client-sso-oidc": { "version": "3.693.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -9090,9 +18985,10 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/client-sts": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/client-sts": { "version": "3.693.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -9139,7 +19035,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/core": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/core": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9159,7 +19055,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/credential-provider-http": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-http": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9178,7 +19074,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/credential-provider-ini": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-ini": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9202,7 +19098,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/credential-provider-node": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9223,7 +19119,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/credential-provider-sso": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-sso": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9240,7 +19136,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/credential-provider-web-identity": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9257,7 +19153,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/middleware-host-header": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-host-header": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9270,7 +19166,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-logger": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9282,7 +19178,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9295,7 +19191,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/middleware-user-agent": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9311,7 +19207,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/region-config-resolver": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/region-config-resolver": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9326,7 +19222,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/token-providers": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/token-providers": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9343,7 +19239,7 @@ "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/util-endpoints": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-endpoints": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9356,7 +19252,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/util-user-agent-browser": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9366,7 +19262,7 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@aws-sdk/util-user-agent-node": { + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9388,7 +19284,7 @@ } } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-sfn/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -9398,7 +19294,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-sfn/node_modules/@smithy/util-buffer-from": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -9409,7 +19305,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sagemaker/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-sfn/node_modules/@smithy/util-utf8": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -9420,7 +19316,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn": { + "node_modules/@aws-sdk/client-ssm": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9464,6 +19360,7 @@ "@smithy/util-middleware": "^3.0.9", "@smithy/util-retry": "^3.0.9", "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.8", "@types/uuid": "^9.0.1", "tslib": "^2.6.2", "uuid": "^9.0.1" @@ -9472,7 +19369,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/client-sso": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/client-sso": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9519,9 +19416,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/client-sso-oidc": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/client-sso-oidc": { "version": "3.693.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -9570,9 +19468,10 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/client-sts": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/client-sts": { "version": "3.693.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -9619,7 +19518,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/core": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/core": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9639,7 +19538,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-http": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-http": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9658,7 +19557,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-ini": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-ini": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9682,7 +19581,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-node": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9703,7 +19602,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-sso": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-sso": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9720,7 +19619,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/credential-provider-web-identity": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9737,7 +19636,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-host-header": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/middleware-host-header": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9750,7 +19649,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/middleware-logger": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9762,7 +19661,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9775,7 +19674,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/middleware-user-agent": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9791,7 +19690,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/region-config-resolver": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/region-config-resolver": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9806,7 +19705,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/token-providers": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/token-providers": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9823,7 +19722,7 @@ "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-endpoints": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/util-endpoints": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9836,7 +19735,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-user-agent-browser": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9846,7 +19745,7 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-user-agent-node": { + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -9868,7 +19767,7 @@ } } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -9878,7 +19777,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/util-buffer-from": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -9889,7 +19788,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/util-utf8": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -9900,207 +19799,397 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ssm": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sso": { + "version": "3.637.0", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.693.0", - "@aws-sdk/client-sts": "3.693.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.1.8", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/client-sso": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.637.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.637.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/types": { + "version": "3.609.0", "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { + "version": "3.609.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.637.0", + "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", + "@aws-sdk/client-sso-oidc": "3.637.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/types": { + "version": "3.609.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.635.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.4.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.730.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@aws-sdk/types": { + "version": "3.723.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@smithy/property-provider": { + "version": "4.0.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/client-sts": { + "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@smithy/types": { + "version": "4.3.1", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.693.0", "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", + "@smithy/property-provider": "^3.1.9", "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/core": { + "node_modules/@aws-sdk/credential-provider-env/node_modules/@aws-sdk/core": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -10120,815 +20209,770 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.635.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/util-stream": "^3.3.0", + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-http/node_modules/@aws-sdk/types": { + "version": "3.609.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-env": "3.693.0", - "@aws-sdk/credential-provider-http": "3.693.0", - "@aws-sdk/credential-provider-process": "3.693.0", - "@aws-sdk/credential-provider-sso": "3.693.0", - "@aws-sdk/credential-provider-web-identity": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/credential-provider-imds": "^3.2.6", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.693.0", - "@aws-sdk/credential-provider-http": "3.693.0", - "@aws-sdk/credential-provider-ini": "3.693.0", - "@aws-sdk/credential-provider-process": "3.693.0", - "@aws-sdk/credential-provider-sso": "3.693.0", - "@aws-sdk/credential-provider-web-identity": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/credential-provider-imds": "^3.2.6", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.758.0", + "@aws-sdk/credential-provider-web-identity": "3.758.0", + "@aws-sdk/nested-clients": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/client-sso": { + "version": "3.758.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.693.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/token-providers": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@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.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/core": { + "version": "3.758.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/types": "^3.7.0", + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.5", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.758.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/middleware-logger": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.758.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.758.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.758.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@smithy/core": "^2.5.2", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", + "@aws-sdk/client-sso": "3.758.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/token-providers": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.734.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/types": "^3.7.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.9", + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/token-providers": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-logger": { + "version": "3.734.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/util-endpoints": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.734.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "@smithy/util-endpoints": "^2.1.5", + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.758.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "bowser": "^2.11.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@smithy/core": "^3.1.5", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.734.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/types": "^3.7.0", + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/token-providers": { + "version": "3.758.0", "license": "Apache-2.0", "dependencies": { + "@aws-sdk/nested-clients": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/types": { + "version": "3.734.0", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/util-utf8": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-endpoints": { + "version": "3.743.0", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "@smithy/util-endpoints": "^3.0.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.637.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.734.0", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.635.0", - "@aws-sdk/middleware-host-header": "3.620.0", - "@aws-sdk/middleware-logger": "3.609.0", - "@aws-sdk/middleware-recursion-detection": "3.620.0", - "@aws-sdk/middleware-user-agent": "3.637.0", - "@aws-sdk/region-config-resolver": "3.614.0", - "@aws-sdk/types": "3.609.0", - "@aws-sdk/util-endpoints": "3.637.0", - "@aws-sdk/util-user-agent-browser": "3.609.0", - "@aws-sdk/util-user-agent-node": "3.614.0", - "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.4.0", - "@smithy/fetch-http-handler": "^3.2.4", - "@smithy/hash-node": "^3.0.3", - "@smithy/invalid-dependency": "^3.0.3", - "@smithy/middleware-content-length": "^3.0.5", - "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", - "@smithy/middleware-serde": "^3.0.3", - "@smithy/middleware-stack": "^3.0.3", - "@smithy/node-config-provider": "^3.1.4", - "@smithy/node-http-handler": "^3.1.4", - "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", - "@smithy/types": "^3.3.0", - "@smithy/url-parser": "^3.0.3", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.15", - "@smithy/util-defaults-mode-node": "^3.0.15", - "@smithy/util-endpoints": "^2.0.5", - "@smithy/util-middleware": "^3.0.3", - "@smithy/util-retry": "^3.0.3", - "@smithy/util-utf8": "^3.0.0", + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.758.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.637.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/abort-controller": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.635.0", - "@aws-sdk/credential-provider-node": "3.637.0", - "@aws-sdk/middleware-host-header": "3.620.0", - "@aws-sdk/middleware-logger": "3.609.0", - "@aws-sdk/middleware-recursion-detection": "3.620.0", - "@aws-sdk/middleware-user-agent": "3.637.0", - "@aws-sdk/region-config-resolver": "3.614.0", - "@aws-sdk/types": "3.609.0", - "@aws-sdk/util-endpoints": "3.637.0", - "@aws-sdk/util-user-agent-browser": "3.609.0", - "@aws-sdk/util-user-agent-node": "3.614.0", - "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.4.0", - "@smithy/fetch-http-handler": "^3.2.4", - "@smithy/hash-node": "^3.0.3", - "@smithy/invalid-dependency": "^3.0.3", - "@smithy/middleware-content-length": "^3.0.5", - "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", - "@smithy/middleware-serde": "^3.0.3", - "@smithy/middleware-stack": "^3.0.3", - "@smithy/node-config-provider": "^3.1.4", - "@smithy/node-http-handler": "^3.1.4", - "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", - "@smithy/types": "^3.3.0", - "@smithy/url-parser": "^3.0.3", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.15", - "@smithy/util-defaults-mode-node": "^3.0.15", - "@smithy/util-endpoints": "^2.0.5", - "@smithy/util-middleware": "^3.0.3", - "@smithy/util-retry": "^3.0.3", - "@smithy/util-utf8": "^3.0.0", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/config-resolver": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.637.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/types": { - "version": "3.609.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/core": { + "version": "3.1.5", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.1.2", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.4", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/credential-provider-imds": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.1.0", - "@smithy/querystring-builder": "^3.0.3", - "@smithy/types": "^3.3.0", - "@smithy/util-base64": "^3.0.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.1", "license": "Apache-2.0", "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/hash-node": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/invalid-dependency": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { - "version": "3.609.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.4", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-content-length": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.1.0", - "@smithy/querystring-builder": "^3.0.3", - "@smithy/types": "^3.3.0", - "@smithy/util-base64": "^3.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-endpoint": { + "version": "4.0.6", "license": "Apache-2.0", "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-retry": { + "version": "4.0.7", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/service-error-classification": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-serde": { + "version": "4.0.2", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-stack": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sts": { - "version": "3.637.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.637.0", - "@aws-sdk/core": "3.635.0", - "@aws-sdk/credential-provider-node": "3.637.0", - "@aws-sdk/middleware-host-header": "3.620.0", - "@aws-sdk/middleware-logger": "3.609.0", - "@aws-sdk/middleware-recursion-detection": "3.620.0", - "@aws-sdk/middleware-user-agent": "3.637.0", - "@aws-sdk/region-config-resolver": "3.614.0", - "@aws-sdk/types": "3.609.0", - "@aws-sdk/util-endpoints": "3.637.0", - "@aws-sdk/util-user-agent-browser": "3.609.0", - "@aws-sdk/util-user-agent-node": "3.614.0", - "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.4.0", - "@smithy/fetch-http-handler": "^3.2.4", - "@smithy/hash-node": "^3.0.3", - "@smithy/invalid-dependency": "^3.0.3", - "@smithy/middleware-content-length": "^3.0.5", - "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", - "@smithy/middleware-serde": "^3.0.3", - "@smithy/middleware-stack": "^3.0.3", - "@smithy/node-config-provider": "^3.1.4", - "@smithy/node-http-handler": "^3.1.4", - "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", - "@smithy/types": "^3.3.0", - "@smithy/url-parser": "^3.0.3", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.15", - "@smithy/util-defaults-mode-node": "^3.0.15", - "@smithy/util-endpoints": "^2.0.5", - "@smithy/util-middleware": "^3.0.3", - "@smithy/util-retry": "^3.0.3", - "@smithy/util-utf8": "^3.0.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/types": { - "version": "3.609.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/node-http-handler": { + "version": "4.0.3", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sts/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.4", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/property-provider": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.1.0", - "@smithy/querystring-builder": "^3.0.3", - "@smithy/types": "^3.3.0", - "@smithy/util-base64": "^3.0.0", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sts/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/protocol-http": { + "version": "5.0.1", "license": "Apache-2.0", "dependencies": { + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-utf8": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/querystring-builder": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@smithy/types": "^4.1.0", + "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/querystring-parser": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/core": { - "version": "3.635.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/service-error-classification": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^2.4.0", - "@smithy/node-config-provider": "^3.1.4", - "@smithy/property-provider": "^3.1.3", - "@smithy/protocol-http": "^4.1.0", - "@smithy/signature-v4": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", - "@smithy/types": "^3.3.0", - "@smithy/util-middleware": "^3.0.3", - "fast-xml-parser": "4.4.1", + "@smithy/types": "^4.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-cognito-identity": { - "version": "3.730.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/signature-v4": { + "version": "5.0.1", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-cognito-identity": "3.730.0", - "@aws-sdk/types": "3.723.0", - "@smithy/property-provider": "^4.0.0", - "@smithy/types": "^4.0.0", + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@aws-sdk/types": { - "version": "3.723.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/smithy-client": { + "version": "4.1.6", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.0.0", + "@smithy/core": "^3.1.5", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@smithy/property-provider": { - "version": "4.0.4", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/types": { + "version": "4.1.0", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@smithy/types": { - "version": "4.3.1", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/url-parser": { + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-base64": { + "version": "4.0.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/types": "^3.7.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-env/node_modules/@aws-sdk/core": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/core": "^2.5.2", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/property-provider": "^3.1.9", - "@smithy/protocol-http": "^4.1.6", - "@smithy/signature-v4": "^4.2.2", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/util-middleware": "^3.0.9", - "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.635.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.609.0", - "@smithy/fetch-http-handler": "^3.2.4", - "@smithy/node-http-handler": "^3.1.4", - "@smithy/property-provider": "^3.1.3", - "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", - "@smithy/types": "^3.3.0", - "@smithy/util-stream": "^3.1.3", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-http/node_modules/@aws-sdk/types": { - "version": "3.609.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.4", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.1.0", - "@smithy/querystring-builder": "^3.0.3", - "@smithy/types": "^3.3.0", - "@smithy/util-base64": "^3.0.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.7", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-env": "3.758.0", - "@aws-sdk/credential-provider-http": "3.758.0", - "@aws-sdk/credential-provider-process": "3.758.0", - "@aws-sdk/credential-provider-sso": "3.758.0", - "@aws-sdk/credential-provider-web-identity": "3.758.0", - "@aws-sdk/nested-clients": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/credential-provider-imds": "^4.0.1", "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/client-sso": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.7", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.5", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-retry": "^4.0.7", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/middleware-stack": "^4.0.1", + "@smithy/credential-provider-imds": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/protocol-http": "^5.0.1", + "@smithy/property-provider": "^4.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@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.7", - "@smithy/util-defaults-mode-node": "^4.0.7", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/core": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-endpoints": { + "version": "3.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-middleware": { + "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, @@ -10936,196 +20980,278 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-retry": { + "version": "4.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-stream": { + "version": "4.1.2", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-utf8": { + "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/client-sso": "3.758.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/token-providers": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.936.0.tgz", + "integrity": "sha512-8DVrdRqPyUU66gfV7VZNToh56ZuO5D6agWrkLQE/xbLJOm2RbeRgh6buz7CqV8ipRd6m+zCl9mM4F3osQLZn8Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.936.0", + "@aws-sdk/nested-clients": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/core": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.936.0.tgz", + "integrity": "sha512-eGJ2ySUMvgtOziHhDRDLCrj473RJoL4J1vPjVM3NrKC/fF3/LoHjkut8AAnKmrW6a2uTzNKubigw8dEnpmpERw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", + "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-logger": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", + "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.936.0.tgz", + "integrity": "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.936.0", + "@aws/lambda-invoke-store": "^0.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.936.0.tgz", + "integrity": "sha512-YB40IPa7K3iaYX0lSnV9easDOLPLh+fJyUDF3BH8doX4i1AOSsYn86L4lVldmOaSX+DwiaqKHpvk4wPBdcIPWw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@smithy/core": "^3.1.5", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@smithy/core": "^3.18.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.734.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.936.0.tgz", + "integrity": "sha512-eyj2tz1XmDSLSZQ5xnB7cLTVKkSJnYAEoNDSUNhzWPxrBDYeJzIbatecOKceKCU8NBf8gWWZCK/CSY0mDxMO0A==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.936.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.936.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/token-providers": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", + "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/nested-clients": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/types": { - "version": "3.734.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-endpoints": { - "version": "3.743.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.734.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.936.0.tgz", + "integrity": "sha512-XOEc7PF9Op00pWV2AYCGDSu5iHgYjIO53Py2VUQTIvP7SRCaCsXmA33mjBvC2Ms6FhSyWNa4aK4naUGIz0hQcw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/middleware-user-agent": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -11140,332 +21266,383 @@ } } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/abort-controller": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/config-resolver": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.0.tgz", + "integrity": "sha512-D1jAmAZQYMoPiacfgNf7AWhg3DFN3Wq/vQv3WINt9znwjzHp2x+WzdJFxxj7xZL7V1U79As6G8f7PorMYWBKsQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/abort-controller": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", + "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/core": { - "version": "3.1.5", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/config-resolver": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", + "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/middleware-serde": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-stream": "^4.1.2", - "@smithy/util-utf8": "^4.0.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/credential-provider-imds": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/core": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.5.tgz", + "integrity": "sha512-6gnIz3h+PEPQGDj8MnRSjDvKBah042jEoPgjFGJ4iJLBE78L4lY/n98x14XyPF4u3lN179Ub/ZKFY5za9GeLQw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/credential-provider-imds": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", + "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/hash-node": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/fetch-http-handler": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", + "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/invalid-dependency": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/hash-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", + "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.9.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/invalid-dependency": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", + "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-content-length": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/middleware-content-length": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", + "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-middleware": "^4.0.1", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-retry": { - "version": "4.0.7", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/middleware-endpoint": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.12.tgz", + "integrity": "sha512-9pAX/H+VQPzNbouhDhkW723igBMLgrI8OtX+++M7iKJgg/zY/Ig3i1e6seCcx22FWhE6Q/S61BRdi2wXBORT+A==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/service-error-classification": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/core": "^3.18.5", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/middleware-retry": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.12.tgz", + "integrity": "sha512-S4kWNKFowYd0lID7/DBqWHOQxmxlsf0jBaos9chQZUWTVOjSW1Ogyh8/ib5tM+agFDJ/TCxuCTvrnlc+9cIBcQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/middleware-serde": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", + "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/middleware-stack": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", + "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/node-config-provider": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", + "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/abort-controller": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/property-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/node-http-handler": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", + "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/abort-controller": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/property-provider": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", + "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/querystring-builder": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/protocol-http": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", + "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", - "@smithy/util-uri-escape": "^4.0.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/querystring-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/querystring-builder": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", + "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.9.0", + "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/service-error-classification": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/querystring-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", + "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0" + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/service-error-classification": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", + "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.9.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", + "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/signature-v4": { - "version": "5.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/signature-v4": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", + "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/smithy-client": { - "version": "4.1.6", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/smithy-client": { + "version": "4.9.8", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.8.tgz", + "integrity": "sha512-8xgq3LgKDEFoIrLWBho/oYKyWByw9/corz7vuh1upv7ZBm0ZMjGYBhbn6v643WoIqA9UTcx5A5htEp/YatUwMA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@smithy/core": "^3.18.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/types": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", + "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -11473,36 +21650,39 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/url-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/url-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", + "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/querystring-parser": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-base64": { - "version": "4.0.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -11510,10 +21690,11 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -11521,22 +21702,24 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-config-provider": { - "version": "4.0.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -11544,55 +21727,58 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.7", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.11.tgz", + "integrity": "sha512-yHv+r6wSQXEXTPVCIQTNmXVWs7ekBTpMVErjqZoWkYN75HIFN5y9+/+sYOejfAuvxWGvgzgxbTHa/oz61YTbKw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.7", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.14.tgz", + "integrity": "sha512-ljZN3iRvaJUgulfvobIuG97q1iUuCMrvXAlkZ4msY+ZuVHQHDIqn7FKZCEj+bx8omz6kF5yQXms/xhzjIO5XiA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/config-resolver": "^4.0.1", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-endpoints": { - "version": "3.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-endpoints": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", + "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -11600,53 +21786,57 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-middleware": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-middleware": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", + "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-retry": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-retry": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", + "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/service-error-classification": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-stream": { - "version": "4.1.2", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-stream": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", + "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -11654,18 +21844,49 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-utf8": { - "version": "4.0.0", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/@aws-sdk/credential-provider-node": { "version": "3.637.0", "license": "Apache-2.0", @@ -11829,7 +22050,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.758.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/nested-clients": "3.758.0", @@ -11845,7 +22065,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/core": { "version": "3.758.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/core": "^3.1.5", @@ -11866,7 +22085,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/types": { "version": "3.734.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -11878,7 +22096,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/abort-controller": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -11890,7 +22107,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/core": { "version": "3.1.5", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", @@ -11908,7 +22124,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/fetch-http-handler": { "version": "5.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", @@ -11923,7 +22138,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/is-array-buffer": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -11934,7 +22148,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-endpoint": { "version": "4.0.6", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-serde": "^4.0.2", @@ -11952,7 +22165,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-serde": { "version": "4.0.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -11964,7 +22176,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-stack": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -11976,7 +22187,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/node-config-provider": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -11990,7 +22200,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/node-http-handler": { "version": "4.0.3", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", @@ -12005,7 +22214,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/property-provider": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -12017,7 +22225,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/protocol-http": { "version": "5.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -12029,7 +22236,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/querystring-builder": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", @@ -12042,7 +22248,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/querystring-parser": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -12054,7 +22259,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/shared-ini-file-loader": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -12066,7 +22270,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/signature-v4": { "version": "5.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "@smithy/protocol-http": "^5.0.1", @@ -12084,7 +22287,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/smithy-client": { "version": "4.1.6", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-endpoint": "^4.0.6", @@ -12101,7 +22303,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/types": { "version": "4.1.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -12112,7 +22313,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/url-parser": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/querystring-parser": "^4.0.1", "@smithy/types": "^4.1.0", @@ -12125,7 +22325,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-base64": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", @@ -12138,7 +22337,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-body-length-browser": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -12149,7 +22347,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-buffer-from": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" @@ -12161,7 +22358,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-hex-encoding": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -12172,7 +22368,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-middleware": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -12184,7 +22379,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-stream": { "version": "4.1.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", @@ -12202,7 +22396,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-uri-escape": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -12213,7 +22406,6 @@ "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-utf8": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" @@ -13379,6 +23571,189 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/middleware-sdk-s3-control": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3-control/-/middleware-sdk-s3-control-3.848.0.tgz", + "integrity": "sha512-1zozD+IKFzFE9RLOCBOGPjhi+jUj0bLxf0ntqBMBJKX9Cf5zqvVuck7mCY19+m0/B+GuSAoiQm2yPV6dcgN17g==", + "dependencies": { + "@aws-sdk/middleware-bucket-endpoint": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-endpoints": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3-control/node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.840.0.tgz", + "integrity": "sha512-+gkQNtPwcSMmlwBHFd4saVVS11In6ID1HczNzpM3MXKXRBfSlbZJbCt6wN//AZ8HMklZEik4tcEOG0qa9UY8SQ==", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3-control/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==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3-control/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", + "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3-control/node_modules/@aws-sdk/util-endpoints": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.848.0.tgz", + "integrity": "sha512-fY/NuFFCq/78liHvRyFKr+aqq1aA/uuVSANjzr5Ym8c+9Z3HRPE9OrExAHoMrZ6zC8tHerQwlsXYYH5XZ7H+ww==", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-endpoints": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3-control/node_modules/@smithy/node-config-provider": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", + "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", + "dependencies": { + "@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/middleware-sdk-s3-control/node_modules/@smithy/property-provider": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", + "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3-control/node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", + "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3-control/node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3-control/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", + "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3-control/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/middleware-sdk-s3-control/node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", + "dependencies": { + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3-control/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3-control/node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", + "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", + "dependencies": { + "@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/middleware-sdk-s3/node_modules/@aws-sdk/core": { "version": "3.693.0", "license": "Apache-2.0", @@ -13687,7 +24062,6 @@ "node_modules/@aws-sdk/nested-clients": { "version": "3.758.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -13735,7 +24109,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/core": { "version": "3.758.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/core": "^3.1.5", @@ -13756,7 +24129,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-host-header": { "version": "3.734.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", @@ -13770,7 +24142,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-logger": { "version": "3.734.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", @@ -13783,7 +24154,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.734.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", @@ -13797,7 +24167,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-user-agent": { "version": "3.758.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", @@ -13814,7 +24183,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/region-config-resolver": { "version": "3.734.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/node-config-provider": "^4.0.1", @@ -13830,7 +24198,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/types": { "version": "3.734.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -13842,7 +24209,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { "version": "3.743.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", @@ -13856,7 +24222,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.734.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", @@ -13867,7 +24232,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.758.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/types": "3.734.0", @@ -13890,7 +24254,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/abort-controller": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -13902,7 +24265,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/config-resolver": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", @@ -13917,7 +24279,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/core": { "version": "3.1.5", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", @@ -13935,7 +24296,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/credential-provider-imds": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", @@ -13950,7 +24310,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/fetch-http-handler": { "version": "5.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", @@ -13965,7 +24324,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/hash-node": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-buffer-from": "^4.0.0", @@ -13979,7 +24337,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/invalid-dependency": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -13991,7 +24348,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/is-array-buffer": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -14002,7 +24358,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-content-length": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", @@ -14015,7 +24370,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-endpoint": { "version": "4.0.6", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-serde": "^4.0.2", @@ -14033,7 +24387,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-retry": { "version": "4.0.7", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", @@ -14052,7 +24405,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-serde": { "version": "4.0.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -14064,7 +24416,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-stack": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -14076,7 +24427,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-config-provider": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -14090,7 +24440,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { "version": "4.0.3", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", @@ -14105,7 +24454,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/property-provider": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -14117,7 +24465,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/protocol-http": { "version": "5.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -14129,7 +24476,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/querystring-builder": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", @@ -14142,7 +24488,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/querystring-parser": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -14154,7 +24499,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/service-error-classification": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0" }, @@ -14165,7 +24509,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/shared-ini-file-loader": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -14177,7 +24520,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/signature-v4": { "version": "5.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "@smithy/protocol-http": "^5.0.1", @@ -14195,7 +24537,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/smithy-client": { "version": "4.1.6", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-endpoint": "^4.0.6", @@ -14212,7 +24553,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/types": { "version": "4.1.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -14223,7 +24563,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/url-parser": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/querystring-parser": "^4.0.1", "@smithy/types": "^4.1.0", @@ -14236,7 +24575,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-base64": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", @@ -14249,7 +24587,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-body-length-browser": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -14260,7 +24597,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-body-length-node": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -14271,7 +24607,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-buffer-from": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" @@ -14283,7 +24618,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-config-provider": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -14294,7 +24628,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-defaults-mode-browser": { "version": "4.0.7", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/smithy-client": "^4.1.6", @@ -14309,7 +24642,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-defaults-mode-node": { "version": "4.0.7", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/config-resolver": "^4.0.1", "@smithy/credential-provider-imds": "^4.0.1", @@ -14326,7 +24658,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-endpoints": { "version": "3.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", @@ -14339,7 +24670,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-hex-encoding": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -14350,7 +24680,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-middleware": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -14362,7 +24691,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-retry": { "version": "4.0.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/service-error-classification": "^4.0.1", "@smithy/types": "^4.1.0", @@ -14375,7 +24703,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-stream": { "version": "4.1.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", @@ -14393,7 +24720,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-uri-escape": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -14404,7 +24730,6 @@ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-utf8": { "version": "4.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" @@ -14711,9 +25036,9 @@ } }, "node_modules/@aws-toolkits/telemetry": { - "version": "1.0.329", - "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.329.tgz", - "integrity": "sha512-zMkljZDtIAxuZzPTLL5zIxn+zGmk767sbqGIc2ZYuv0sSU+UoYgB3tqwV5KVV2oDPKs5593nwJC97NVHJqzowQ==", + "version": "1.0.338", + "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.338.tgz", + "integrity": "sha512-fg3zCqH4GEBjgL3Wo+xiijRbkyxMh4hXPsOD8Q52k8bvmq5rL9tjbp2IqmHI8JVLOhoomedicXK9ZVEdKSsatw==", "dev": true, "dependencies": { "ajv": "^6.12.6", @@ -14743,14 +25068,22 @@ "@aws/language-server-runtimes-types": "^0.1.41" } }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", + "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.125", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.125.tgz", - "integrity": "sha512-tjXJEagZ6rm09fcgJGu1zbFNzi0+7R1mdNFa6zCIv68c76xq5JHjc++Hne9aOgp61O6BM9uNnX3KR57v9/0E1g==", - "dev": true, + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.3.5.tgz", + "integrity": "sha512-42Ed8O3NMUgZnOZugWCR3uNu2K4cQ7LO3DHMZPQlxpiR7BiyoUo572UMTX9HZyFNErunqdtb0SBPmrdQSLCljQ==", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes-types": "^0.1.56", + "@aws/language-server-runtimes-types": "^0.1.61", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.200.0", "@opentelemetry/core": "^2.0.0", @@ -14761,7 +25094,6 @@ "@opentelemetry/sdk-metrics": "^2.0.1", "@smithy/node-http-handler": "^4.0.4", "ajv": "^8.17.1", - "aws-sdk": "^2.1692.0", "hpagent": "^1.2.0", "jose": "^5.9.6", "mac-ca": "^3.1.1", @@ -14773,14 +25105,13 @@ "win-ca": "^3.5.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=24.0.0" } }, "node_modules/@aws/language-server-runtimes-types": { - "version": "0.1.56", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.56.tgz", - "integrity": "sha512-Md/L750JShCHUsCQUJva51Ofkn/GDBEX8PpZnWUIVqkpddDR00SLQS2smNf4UHtKNJ2fefsfks/Kqfuatjkjvg==", - "dev": true, + "version": "0.1.61", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.61.tgz", + "integrity": "sha512-kRBcbNDZrJtw3UFqcJ60tYfxM/DzDCHQEz38HINvyecfDCHRTpAAebOMoRQ7PagmsPJ4tasEwzEyRSg2vxq6aQ==", "license": "Apache-2.0", "dependencies": { "vscode-languageserver-textdocument": "^1.0.12", @@ -14789,8 +25120,8 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/@opentelemetry/core": { "version": "2.0.1", - "dev": true, - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", + "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -14803,8 +25134,8 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/@opentelemetry/resources": { "version": "2.0.1", - "dev": true, - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -14818,8 +25149,8 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/@opentelemetry/sdk-metrics": { "version": "2.0.1", - "dev": true, - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", + "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" @@ -14833,7 +25164,8 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/@smithy/abort-controller": { "version": "4.0.2", - "dev": true, + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.2.tgz", + "integrity": "sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.2.0", @@ -14845,7 +25177,8 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/@smithy/node-http-handler": { "version": "4.0.4", - "dev": true, + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.4.tgz", + "integrity": "sha512-/mdqabuAT3o/ihBGjL94PUbTSPSRJ0eeVTdgADzow0wRJ0rN4A27EOrtlK56MYiO1fDvlO3jVTCxQtQmK9dZ1g==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.0.2", @@ -14860,7 +25193,8 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/@smithy/protocol-http": { "version": "5.1.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.2.0", @@ -14872,7 +25206,8 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/@smithy/querystring-builder": { "version": "4.0.2", - "dev": true, + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.2.tgz", + "integrity": "sha512-NTOs0FwHw1vimmQM4ebh+wFQvOwkEf/kQL6bSM1Lock+Bv4I89B3hGYoUEPkmvYPkDKyp5UdXJYu+PoTQ3T31Q==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.2.0", @@ -14885,7 +25220,8 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/@smithy/types": { "version": "4.2.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.2.0.tgz", + "integrity": "sha512-7eMk09zQKCO+E/ivsjQv+fDlOupcFUCSC/L2YUPgwhvowVGWbPQHjEFcmjt7QQ4ra5lyowS92SV53Zc6XD4+fg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -14896,7 +25232,8 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/@smithy/util-uri-escape": { "version": "4.0.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -14907,7 +25244,6 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/ajv": { "version": "8.17.1", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -14922,7 +25258,6 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/jose": { "version": "5.10.0", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -14930,41 +25265,12 @@ }, "node_modules/@aws/language-server-runtimes/node_modules/json-schema-traverse": { "version": "1.0.0", - "dev": true, "license": "MIT" }, - "node_modules/@aws/language-server-runtimes/node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/vscode-languageserver": { - "version": "9.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/@aws/language-server-runtimes/node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "dev": true, - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, "node_modules/@aws/language-server-runtimes/node_modules/vscode-uri": { "version": "3.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==" }, "node_modules/@aws/mynah-ui": { "version": "4.35.4", @@ -15736,6 +26042,46 @@ "dev": true, "license": "MIT" }, + "node_modules/@kubernetes/client-node": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.20.0.tgz", + "integrity": "sha512-xxlv5GLX4FVR/dDKEsmi4SPeuB49aRc35stndyxcC73XnUEEwF39vXbROpHOirmDse8WE9vxOjABnSVS+jb7EA==", + "license": "Apache-2.0", + "dependencies": { + "@types/js-yaml": "^4.0.1", + "@types/node": "^20.1.1", + "@types/request": "^2.47.1", + "@types/ws": "^8.5.3", + "byline": "^5.0.0", + "isomorphic-ws": "^5.0.0", + "js-yaml": "^4.1.0", + "jsonpath-plus": "^7.2.0", + "request": "^2.88.0", + "rfc4648": "^1.3.0", + "stream-buffers": "^3.0.2", + "tar": "^6.1.11", + "tslib": "^2.4.1", + "ws": "^8.11.0" + }, + "optionalDependencies": { + "openid-client": "^5.3.0" + } + }, + "node_modules/@kubernetes/client-node/node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@kubernetes/client-node/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "dev": true, @@ -15775,15 +26121,18 @@ }, "node_modules/@opentelemetry/api": { "version": "1.9.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } }, "node_modules/@opentelemetry/api-logs": { "version": "0.200.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.200.0.tgz", + "integrity": "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -15794,7 +26143,8 @@ }, "node_modules/@opentelemetry/core": { "version": "2.0.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", + "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -15808,7 +26158,8 @@ }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { "version": "0.200.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.200.0.tgz", + "integrity": "sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.200.0", @@ -15826,7 +26177,8 @@ }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { "version": "0.200.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.200.0.tgz", + "integrity": "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.0", @@ -15844,7 +26196,8 @@ }, "node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.200.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.200.0.tgz", + "integrity": "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.0", @@ -15859,7 +26212,8 @@ }, "node_modules/@opentelemetry/otlp-transformer": { "version": "0.200.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.200.0.tgz", + "integrity": "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.200.0", @@ -15879,7 +26233,8 @@ }, "node_modules/@opentelemetry/resources": { "version": "2.0.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.0", @@ -15894,7 +26249,8 @@ }, "node_modules/@opentelemetry/sdk-logs": { "version": "0.200.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.200.0.tgz", + "integrity": "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.200.0", @@ -15910,7 +26266,8 @@ }, "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { "version": "2.0.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", + "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -15924,7 +26281,8 @@ }, "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { "version": "2.0.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", + "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.0", @@ -15939,7 +26297,8 @@ }, "node_modules/@opentelemetry/sdk-metrics": { "version": "2.0.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.0.tgz", + "integrity": "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.0", @@ -15954,7 +26313,8 @@ }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "2.0.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.0.tgz", + "integrity": "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.0", @@ -15970,7 +26330,8 @@ }, "node_modules/@opentelemetry/semantic-conventions": { "version": "1.33.0", - "dev": true, + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.33.0.tgz", + "integrity": "sha512-TIpZvE8fiEILFfTlfPnltpBaD3d9/+uQHVCyC3vfdh6WfCXKhNFzoP5RyDDIndfvZC5GrA4pyEDNyjPloJud+w==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -16064,6 +26425,7 @@ "version": "1.14.2", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "clipboardy": "^4.0.0", "clone-deep": "^4.0.1", @@ -16324,7 +26686,9 @@ } }, "node_modules/@sinonjs/text-encoding": { - "version": "0.7.1", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", "dev": true, "license": "(Unlicense OR Apache-2.0)" }, @@ -16657,6 +27021,54 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/middleware-apply-body-checksum": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-apply-body-checksum/-/middleware-apply-body-checksum-4.1.2.tgz", + "integrity": "sha512-YK7yIjjW67Fat8uk2CsUDaQwfcvA1RPaoLKKDZycf7QZ3QlmPUuLLDsMVrJWPy/2mahJjpcaAfzZnK7cXDlVAQ==", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-apply-body-checksum/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-apply-body-checksum/node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", + "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-apply-body-checksum/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/@smithy/middleware-content-length": { "version": "3.0.13", "license": "Apache-2.0", @@ -17142,6 +27554,18 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@stylistic/eslint-plugin": { "version": "2.11.0", "dev": true, @@ -17558,6 +27982,12 @@ "@types/responselike": "*" } }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT" + }, "node_modules/@types/circular-dependency-plugin": { "version": "5.0.8", "dev": true, @@ -17598,11 +28028,11 @@ "license": "MIT" }, "node_modules/@types/eslint": { - "version": "8.44.8", + "version": "8.56.12", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", + "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -17677,7 +28107,6 @@ }, "node_modules/@types/js-yaml": { "version": "4.0.5", - "dev": true, "license": "MIT" }, "node_modules/@types/jsdom": { @@ -17759,6 +28188,7 @@ "node_modules/@types/node": { "version": "22.8.4", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.19.8" } @@ -17810,6 +28240,35 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/@types/responselike": { "version": "1.0.0", "license": "MIT", @@ -17906,7 +28365,6 @@ }, "node_modules/@types/tough-cookie": { "version": "4.0.5", - "dev": true, "license": "MIT" }, "node_modules/@types/uuid": { @@ -17992,6 +28450,7 @@ "version": "7.14.1", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.14.1", "@typescript-eslint/types": "7.14.1", @@ -18450,6 +28909,34 @@ "@vscode/vsce-sign-win32-x64": "2.0.5" } }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.5.tgz", + "integrity": "sha512-XVmnF40APwRPXSLYA28Ye+qWxB25KhSVpF2eZVtVOs6g7fkpOxsVnpRU1Bz2xG4ySI79IRuapDJoAQFkoOgfdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.5.tgz", + "integrity": "sha512-JuxY3xcquRsOezKq6PEHwCgd1rh1GnhyH6urVEWUzWn1c1PC4EOoyffMD+zLZtFuZF5qR1I0+cqDRNKyPvpK7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, "node_modules/@vscode/vsce-sign-darwin-arm64": { "version": "2.0.5", "cpu": [ @@ -18462,6 +28949,90 @@ "darwin" ] }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.5.tgz", + "integrity": "sha512-ma9JDC7FJ16SuPXlLKkvOD2qLsmW/cKfqK4zzM2iJE1PbckF3BlR08lYqHV89gmuoTpYB55+z8Y5Fz4wEJBVDA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.5.tgz", + "integrity": "sha512-cdCwtLGmvC1QVrkIsyzv01+o9eR+wodMJUZ9Ak3owhcGxPRB53/WvrDHAFYA6i8Oy232nuen1YqWeEohqBuSzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.5.tgz", + "integrity": "sha512-Hr1o0veBymg9SmkCqYnfaiUnes5YK6k/lKFA5MhNmiEN5fNqxyPUCdRZMFs3Ajtx2OFW4q3KuYVRwGA7jdLo7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.5.tgz", + "integrity": "sha512-XLT0gfGMcxk6CMRLDkgqEPTyG8Oa0OFe1tPv2RVbphSOjFWJwZgK3TYWx39i/7gqpDHlax0AP6cgMygNJrA6zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.5.tgz", + "integrity": "sha512-hco8eaoTcvtmuPhavyCZhrk5QIcLiyAUhEso87ApAWDllG7djIrWiOCtqn48k4pHz+L8oCQlE0nwNHfcYcxOPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.5.tgz", + "integrity": "sha512-1ixKFGM2FwM+6kQS2ojfY3aAelICxjiCzeg4nTHpkeU1Tfs4RC+lVLrgq5NwcBC7ZLr6UfY3Ct3D6suPeOf7BQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@vscode/vsce/node_modules/ansi-styles": { "version": "3.2.1", "dev": true, @@ -18908,6 +29479,7 @@ "version": "8.14.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -18959,8 +29531,8 @@ }, "node_modules/ajv": { "version": "6.12.6", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -19034,33 +29606,10 @@ "yaml-language-server": "0.15.0" } }, - "node_modules/amazon-states-language-service/node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/amazon-states-language-service/node_modules/vscode-languageserver": { - "version": "9.0.1", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/amazon-states-language-service/node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, "node_modules/ansi-colors": { "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "license": "MIT", "engines": { "node": ">=6" @@ -19068,6 +29617,8 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "license": "MIT", "dependencies": { @@ -19082,6 +29633,8 @@ }, "node_modules/ansi-escapes/node_modules/type-fest": { "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -19160,7 +29713,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, "license": "ISC" }, "node_modules/are-we-there-yet": { @@ -19168,7 +29720,6 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", "deprecated": "This package is no longer supported.", - "dev": true, "license": "ISC", "dependencies": { "delegates": "^1.0.0", @@ -19179,7 +29730,6 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -19195,14 +29745,12 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/are-we-there-yet/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -19235,6 +29783,15 @@ "dev": true, "license": "MIT" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/asn1.js": { "version": "5.4.1", "license": "MIT", @@ -19261,6 +29818,15 @@ "util": "^0.12.5" } }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ast-types": { "version": "0.9.14", "dev": true, @@ -19287,7 +29853,6 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "dev": true, "license": "MIT" }, "node_modules/available-typed-arrays": { @@ -19327,20 +29892,128 @@ "node": ">= 10.0.0" } }, - "node_modules/aws-sdk/node_modules/buffer": { - "version": "4.9.2", + "node_modules/aws-sdk-client-mock": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/aws-sdk-client-mock/-/aws-sdk-client-mock-4.1.0.tgz", + "integrity": "sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw==", + "dev": true, "license": "MIT", "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" + "@types/sinon": "^17.0.3", + "sinon": "^18.0.1", + "tslib": "^2.1.0" } }, - "node_modules/aws-sdk/node_modules/events": { - "version": "1.1.1", + "node_modules/aws-sdk-client-mock/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/aws-sdk-client-mock/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/aws-sdk-client-mock/node_modules/@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "node_modules/aws-sdk-client-mock/node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.4.x" + "node": ">=4" + } + }, + "node_modules/aws-sdk-client-mock/node_modules/@types/sinon": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/aws-sdk-client-mock/node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-sdk-client-mock/node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, + "node_modules/aws-sdk-client-mock/node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/aws-sdk-client-mock/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/aws-sdk-client-mock/node_modules/sinon": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.1.tgz", + "integrity": "sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" } }, "node_modules/aws-sdk/node_modules/uuid": { @@ -19350,6 +30023,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/aws-ssm-document-language-service": { "version": "1.0.0", "license": "Apache-2.0", @@ -19411,6 +30093,18 @@ "node": ">=8.0.0 || >=10.0.0" } }, + "node_modules/aws-ssm-document-language-service/node_modules/vscode-languageserver": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz", + "integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "^3.15.3" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, "node_modules/aws-ssm-document-language-service/node_modules/vscode-languageserver-protocol": { "version": "3.14.1", "license": "MIT", @@ -19423,6 +30117,25 @@ "version": "3.14.0", "license": "MIT" }, + "node_modules/aws-ssm-document-language-service/node_modules/vscode-languageserver/node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/aws-ssm-document-language-service/node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, "node_modules/aws-ssm-document-language-service/node_modules/vscode-nls": { "version": "4.1.2", "license": "MIT" @@ -19470,6 +30183,12 @@ "resolved": "packages/toolkit", "link": true }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, "node_modules/azure-devops-node-api": { "version": "11.2.0", "dev": true, @@ -19568,6 +30287,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/big.js": { "version": "5.2.2", "dev": true, @@ -19600,7 +30328,6 @@ }, "node_modules/bl": { "version": "4.1.0", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -19610,7 +30337,6 @@ }, "node_modules/bl/node_modules/buffer": { "version": "5.7.1", - "dev": true, "funding": [ { "type": "github", @@ -19831,6 +30557,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001629", "electron-to-chromium": "^1.4.796", @@ -19890,6 +30617,8 @@ }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true, "license": "BSD-3-Clause" }, @@ -19938,6 +30667,8 @@ }, "node_modules/bundle-name": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -19950,6 +30681,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "license": "MIT", @@ -20031,7 +30771,7 @@ "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" + "set-function-length": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -20042,6 +30782,8 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -20053,6 +30795,8 @@ }, "node_modules/call-bound": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -20169,6 +30913,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "4.1.2", "license": "MIT", @@ -20255,7 +31005,6 @@ }, "node_modules/chownr": { "version": "1.1.4", - "dev": true, "license": "ISC" }, "node_modules/chrome-trace-event": { @@ -20613,7 +31362,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20682,7 +31430,6 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -20774,7 +31521,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, "license": "ISC" }, "node_modules/content-disposition": { @@ -21103,6 +31849,18 @@ "type": "^1.0.1" } }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/data-urls": { "version": "5.0.0", "dev": true, @@ -21207,7 +31965,6 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4.0.0" @@ -21308,7 +32065,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -21316,7 +32072,6 @@ }, "node_modules/delegates": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/depd": { @@ -21364,6 +32119,8 @@ }, "node_modules/diff": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -21469,6 +32226,8 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -21560,8 +32319,20 @@ "version": "0.2.0", "license": "MIT" }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -21569,13 +32340,16 @@ } }, "node_modules/editions": { - "version": "6.21.0", + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", "dev": true, "license": "Artistic-2.0", "dependencies": { - "version-range": "^4.13.0" + "version-range": "^4.15.0" }, "engines": { + "ecmascript": ">= es5", "node": ">=4" }, "funding": { @@ -21688,6 +32462,8 @@ }, "node_modules/es-define-property": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -21707,6 +32483,8 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -21715,6 +32493,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es5-ext": { "version": "0.10.64", "dev": true, @@ -21866,6 +32659,7 @@ "version": "8.56.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -21920,6 +32714,7 @@ "version": "9.1.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -22333,7 +33128,6 @@ }, "node_modules/expand-template": { "version": "2.0.3", - "dev": true, "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" @@ -22444,6 +33238,21 @@ "dev": true, "license": "ISC" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, "node_modules/fancy-log": { "version": "1.3.3", "license": "MIT", @@ -22492,7 +33301,6 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -22502,7 +33310,6 @@ }, "node_modules/fast-uri": { "version": "3.0.6", - "dev": true, "funding": [ { "type": "github", @@ -22771,6 +33578,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/form-data": { "version": "4.0.0", "dev": true, @@ -22814,7 +33630,6 @@ }, "node_modules/fs-constants": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/fs-extra": { @@ -22830,6 +33645,30 @@ "node": ">=14.14" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs-monkey": { "version": "1.0.3", "dev": true, @@ -22863,7 +33702,6 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", "deprecated": "This package is no longer supported.", - "dev": true, "license": "ISC", "dependencies": { "aproba": "^1.0.3", @@ -22880,7 +33718,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -22890,7 +33727,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "dev": true, "license": "MIT", "dependencies": { "number-is-nan": "^1.0.0" @@ -22903,7 +33739,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "dev": true, "license": "MIT", "dependencies": { "code-point-at": "^1.0.0", @@ -22918,7 +33753,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^2.0.0" @@ -22941,6 +33775,8 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -22963,6 +33799,8 @@ }, "node_modules/get-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -22996,9 +33834,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/github-from-package": { "version": "0.0.0", - "dev": true, "license": "MIT" }, "node_modules/glob": { @@ -23103,6 +33949,8 @@ }, "node_modules/gopd": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -23165,12 +34013,35 @@ "dev": true, "license": "MIT" }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/hard-rejection": { "version": "2.1.0", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=4" } }, "node_modules/has-flag": { @@ -23192,6 +34063,8 @@ }, "node_modules/has-symbols": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -23202,6 +34075,8 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -23217,7 +34092,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, "license": "ISC" }, "node_modules/hash-base": { @@ -23329,7 +34203,6 @@ }, "node_modules/hpagent": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -23505,6 +34378,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, "node_modules/http2": { "version": "3.3.6", "license": "MIT", @@ -23677,7 +34565,6 @@ }, "node_modules/ini": { "version": "1.3.8", - "dev": true, "license": "ISC" }, "node_modules/interpret": { @@ -23811,7 +34698,6 @@ }, "node_modules/is-electron": { "version": "2.2.2", - "dev": true, "license": "MIT" }, "node_modules/is-extglob": { @@ -23974,6 +34860,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "license": "MIT", @@ -24041,6 +34933,21 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "dev": true, @@ -24174,6 +35081,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, "node_modules/jsdom": { "version": "23.0.1", "dev": true, @@ -24273,6 +35186,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-to-typescript": { "version": "13.1.1", "dev": true, @@ -24313,6 +35232,7 @@ "version": "7.2.3", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -24373,7 +35293,6 @@ }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -24381,6 +35300,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "dev": true, @@ -24414,12 +35339,23 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsonwebtoken": { - "version": "9.0.2", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "dev": true, "license": "MIT", "dependencies": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -24435,6 +35371,21 @@ "npm": ">=6" } }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/jszip": { "version": "3.10.1", "dev": true, @@ -24483,7 +35434,9 @@ "license": "MIT" }, "node_modules/jwa": { - "version": "1.4.2", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, "license": "MIT", "dependencies": { @@ -24493,11 +35446,13 @@ } }, "node_modules/jws": { - "version": "3.2.2", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dev": true, "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -24870,7 +35825,7 @@ }, "node_modules/lru-cache": { "version": "6.0.0", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -24889,7 +35844,6 @@ }, "node_modules/mac-ca": { "version": "3.1.1", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "node-forge": "^1.3.1", @@ -24977,6 +35931,8 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -25256,6 +36212,31 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "license": "MIT", @@ -25268,12 +36249,12 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", - "dev": true, "license": "MIT" }, "node_modules/mocha": { "version": "11.7.1", "license": "MIT", + "peer": true, "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", @@ -25566,7 +36547,6 @@ }, "node_modules/napi-build-utils": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -25666,7 +36646,6 @@ }, "node_modules/node-forge": { "version": "1.3.1", - "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -25711,7 +36690,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", "integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==", - "dev": true, "license": "MIT" }, "node_modules/normalize-package-data": { @@ -25762,7 +36740,6 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "deprecated": "This package is no longer supported.", - "dev": true, "license": "ISC", "dependencies": { "are-we-there-yet": "~1.1.2", @@ -25786,7 +36763,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -25829,14 +36805,32 @@ "dev": true, "license": "MIT" }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/object-assign": { "version": "4.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "dev": true, @@ -25893,6 +36887,16 @@ "dev": true, "license": "MIT" }, + "node_modules/oidc-token-hash": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", + "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "dev": true, @@ -25960,6 +36964,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "optional": true, + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/optionator": { "version": "0.9.3", "dev": true, @@ -26258,6 +37288,12 @@ "dev": true, "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC" @@ -26275,7 +37311,6 @@ }, "node_modules/pify": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -26426,6 +37461,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -26561,6 +37597,7 @@ "version": "3.3.3", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -26697,7 +37734,6 @@ }, "node_modules/psl": { "version": "1.9.0", - "dev": true, "license": "MIT" }, "node_modules/public-encrypt": { @@ -26863,7 +37899,6 @@ }, "node_modules/rc": { "version": "1.2.8", - "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -26888,7 +37923,6 @@ }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -27123,7 +38157,6 @@ "version": "1.16.1", "resolved": "https://registry.npmjs.org/registry-js/-/registry-js-1.16.1.tgz", "integrity": "sha512-pQ2kD36lh+YNtpaXm6HCCb0QZtV/zQEeKnkfEIj5FDSpF/oFts7pwizEUkWSvP8IbGb4A4a5iBhhS9eUearMmQ==", - "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -27135,7 +38168,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^2.0.0" @@ -27148,7 +38180,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, "license": "Apache-2.0", "bin": { "detect-libc": "bin/detect-libc.js" @@ -27161,7 +38192,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -27174,7 +38204,6 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", - "dev": true, "license": "MIT", "dependencies": { "semver": "^5.4.1" @@ -27184,14 +38213,12 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true, "license": "MIT" }, "node_modules/registry-js/node_modules/prebuild-install": { "version": "5.3.6", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.6.tgz", "integrity": "sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg==", - "dev": true, "license": "MIT", "dependencies": { "detect-libc": "^1.0.3", @@ -27221,7 +38248,6 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver" @@ -27231,7 +38257,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "dev": true, "license": "MIT", "dependencies": { "decompress-response": "^4.2.0", @@ -27268,6 +38293,38 @@ "node": ">= 0.10" } }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/request-light": { "version": "0.2.5", "license": "MIT", @@ -27324,6 +38381,52 @@ "version": "4.1.2", "license": "MIT" }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "license": "MIT", @@ -27333,7 +38436,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -27458,6 +38560,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfc4648": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.4.tgz", + "integrity": "sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==", + "license": "MIT" + }, "node_modules/rimraf": { "version": "3.0.2", "dev": true, @@ -27539,7 +38647,6 @@ }, "node_modules/rxjs": { "version": "7.8.2", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -27605,6 +38712,7 @@ "version": "1.69.5", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -27878,6 +38986,7 @@ } ], "license": "Apache-2.0", + "peer": true, "dependencies": { "@bazel/runfiles": "^6.3.1", "jszip": "^3.10.1", @@ -28053,7 +39162,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, "license": "ISC" }, "node_modules/set-function-length": { @@ -28161,12 +39269,10 @@ }, "node_modules/signal-exit": { "version": "3.0.7", - "dev": true, "license": "ISC" }, "node_modules/simple-concat": { "version": "1.0.1", - "dev": true, "funding": [ { "type": "github", @@ -28380,6 +39486,31 @@ "version": "1.0.3", "license": "BSD-3-Clause" }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "license": "MIT", @@ -28809,9 +39940,25 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "2.1.1", - "dev": true, "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -28822,7 +39969,6 @@ }, "node_modules/tar-stream": { "version": "2.2.0", - "dev": true, "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -28835,8 +39981,40 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/targz": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/targz/-/targz-1.0.1.tgz", + "integrity": "sha512-6q4tP9U55mZnRuMTBqnqc3nwYQY3kv+QthCFZuMk+Tn1qYUnMPmL/JZ/mzgXINzFpSqfU+242IFmFU9VPvqaQw==", "dev": true, "license": "MIT", "dependencies": { @@ -28845,6 +40023,8 @@ }, "node_modules/targz/node_modules/bl": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, "license": "MIT", "dependencies": { @@ -28854,6 +40034,8 @@ }, "node_modules/targz/node_modules/pump": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", "dev": true, "license": "MIT", "dependencies": { @@ -28863,6 +40045,8 @@ }, "node_modules/targz/node_modules/readable-stream": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", "dependencies": { @@ -28877,11 +40061,15 @@ }, "node_modules/targz/node_modules/safe-buffer": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "license": "MIT" }, "node_modules/targz/node_modules/string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", "dependencies": { @@ -28889,7 +40077,9 @@ } }, "node_modules/targz/node_modules/tar-fs": { - "version": "1.16.5", + "version": "1.16.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.6.tgz", + "integrity": "sha512-JkOgFt3FxM/2v2CNpAVHqMW2QASjc/Hxo7IGfNd3MHaDYSW/sBFiS7YVmmhmr8x6vwN1VFQDQGdT2MWpmIuVKA==", "dev": true, "license": "MIT", "dependencies": { @@ -28901,6 +40091,8 @@ }, "node_modules/targz/node_modules/tar-stream": { "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "dev": true, "license": "MIT", "dependencies": { @@ -29378,7 +40570,6 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", - "dev": true, "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" @@ -29387,6 +40578,12 @@ "node": "*" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/type": { "version": "1.2.0", "dev": true, @@ -29458,6 +40655,7 @@ "version": "5.2.2", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -29511,7 +40709,6 @@ }, "node_modules/undici": { "version": "6.21.2", - "dev": true, "license": "MIT", "engines": { "node": ">=18.17" @@ -29615,7 +40812,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -29756,8 +40952,30 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, "node_modules/version-range": { - "version": "4.14.0", + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", "dev": true, "license": "Artistic-2.0", "engines": { @@ -30436,52 +41654,71 @@ "license": "MIT" }, "node_modules/vscode-jsonrpc": { - "version": "5.0.1", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "license": "MIT", "engines": { - "node": ">=8.0.0 || >=10.0.0" + "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "6.1.4", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", "license": "MIT", "dependencies": { - "semver": "^6.3.0", - "vscode-languageserver-protocol": "3.15.3" + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" }, "engines": { - "vscode": "^1.41.0" + "vscode": "^1.82.0" } }, - "node_modules/vscode-languageclient/node_modules/semver": { - "version": "6.3.1", + "node_modules/vscode-languageclient/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" } }, "node_modules/vscode-languageserver": { - "version": "6.1.1", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", "license": "MIT", "dependencies": { - "vscode-languageserver-protocol": "^3.15.3" + "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.15.3", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "license": "MIT", "dependencies": { - "vscode-jsonrpc": "^5.0.1", - "vscode-languageserver-types": "3.15.1" + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" } }, - "node_modules/vscode-languageserver-protocol/node_modules/vscode-languageserver-types": { - "version": "3.15.1", - "license": "MIT" - }, "node_modules/vscode-languageserver-textdocument": { "version": "1.0.12", "license": "MIT" @@ -30572,6 +41809,7 @@ "node_modules/vue": { "version": "3.3.4", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.3.4", "@vue/compiler-sfc": "3.3.4", @@ -30748,6 +41986,7 @@ "version": "5.95.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -30793,6 +42032,7 @@ "version": "5.1.4", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -30867,6 +42107,7 @@ "version": "8.11.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -30974,6 +42215,7 @@ "version": "8.8.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -31131,7 +42373,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -31160,7 +42401,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" @@ -31173,7 +42413,6 @@ }, "node_modules/win-ca": { "version": "3.5.1", - "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -31185,7 +42424,6 @@ }, "node_modules/win-ca/node_modules/make-dir": { "version": "1.3.0", - "dev": true, "license": "MIT", "dependencies": { "pify": "^3.0.0" @@ -31309,6 +42547,7 @@ "node_modules/ws": { "version": "8.18.2", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -31378,7 +42617,6 @@ }, "node_modules/yallist": { "version": "4.0.0", - "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -31643,7 +42881,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.89.0-SNAPSHOT", + "version": "1.107.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -31661,7 +42899,9 @@ "dependencies": { "@amzn/amazon-q-developer-streaming-client": "file:../../src.gen/@amzn/amazon-q-developer-streaming-client", "@amzn/codewhisperer-streaming": "file:../../src.gen/@amzn/codewhisperer-streaming", + "@amzn/glue-catalog-client": "file:../../src.gen/@amzn/glue-catalog-client/0.0.1.tgz", "@amzn/sagemaker-client": "file:../../src.gen/@amzn/sagemaker-client/1.0.0.tgz", + "@aws-sdk/client-accessanalyzer": "^3.888.0", "@aws-sdk/client-api-gateway": "<3.731.0", "@aws-sdk/client-apprunner": "<3.731.0", "@aws-sdk/client-cloudcontrol": "<3.731.0", @@ -31669,13 +42909,26 @@ "@aws-sdk/client-cloudwatch-logs": "<3.731.0", "@aws-sdk/client-codecatalyst": "<3.731.0", "@aws-sdk/client-cognito-identity": "<3.731.0", + "@aws-sdk/client-datazone": "^3.848.0", "@aws-sdk/client-docdb": "<3.731.0", "@aws-sdk/client-docdb-elastic": "<3.731.0", "@aws-sdk/client-ec2": "<3.731.0", + "@aws-sdk/client-ecr": "~3.693.0", + "@aws-sdk/client-ecs": "~3.693.0", + "@aws-sdk/client-eks": "^3.583.0", + "@aws-sdk/client-glue": "^3.852.0", "@aws-sdk/client-iam": "<3.731.0", + "@aws-sdk/client-iot": "~3.693.0", + "@aws-sdk/client-iotsecuretunneling": "~3.693.0", "@aws-sdk/client-lambda": "<3.731.0", + "@aws-sdk/client-redshift": "~3.693.0", + "@aws-sdk/client-redshift-data": "~3.693.0", + "@aws-sdk/client-redshift-serverless": "~3.693.0", "@aws-sdk/client-s3": "<3.731.0", + "@aws-sdk/client-s3-control": "^3.830.0", "@aws-sdk/client-sagemaker": "<3.696.0", + "@aws-sdk/client-schemas": "~3.693.0", + "@aws-sdk/client-secrets-manager": "~3.693.0", "@aws-sdk/client-sfn": "<3.731.0", "@aws-sdk/client-ssm": "<3.731.0", "@aws-sdk/client-sso": "<3.731.0", @@ -31693,6 +42946,7 @@ "@aws/mynah-ui": "^4.35.4", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", + "@kubernetes/client-node": "^0.20.0", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/middleware-retry": "^4.0.3", "@smithy/node-http-handler": "^4.0.2", @@ -31735,8 +42989,8 @@ "strip-ansi": "^5.2.0", "svgdom": "^0.1.0", "tcp-port-used": "^1.0.1", - "vscode-languageclient": "^6.1.4", - "vscode-languageserver": "^6.1.1", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.15.3", "vscode-languageserver-textdocument": "^1.0.8", "vue": "^3.3.4", @@ -31785,6 +43039,7 @@ "@types/whatwg-url": "^11.0.4", "@types/xml2js": "^0.4.11", "@vue/compiler-sfc": "^3.3.2", + "aws-sdk-client-mock": "^4.1.0", "c8": "^9.0.0", "circular-dependency-plugin": "^5.2.2", "css-loader": "^6.10.0", @@ -32069,6 +43324,435 @@ "node": ">=16.0.0" } }, + "packages/core/node_modules/@aws-sdk/client-ecs": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ecs/-/client-ecs-3.693.0.tgz", + "integrity": "sha512-HbMtxh+gBtdHS4v0lZk7mb/E9PtjK9m2mDxiqyTXcZkdYPnq3MGACgUNUt8Siv+BgzQJTP8jikflCeMQ4ECHmw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.8", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-ecs/node_modules/@smithy/fetch-http-handler": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.3.tgz", + "integrity": "sha512-6SxNltSncI8s689nvnzZQc/dPXcpHQ34KUj6gR/HBroytKOd/isMG3gJF/zBE1TBmTT18TXyzhg3O3SOOqGEhA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.8", + "@smithy/querystring-builder": "^3.0.11", + "@smithy/types": "^3.7.2", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "packages/core/node_modules/@aws-sdk/client-ecs/node_modules/@smithy/middleware-retry": { + "version": "3.0.34", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.34.tgz", + "integrity": "sha512-yVRr/AAtPZlUvwEkrq7S3x7Z8/xCd97m2hLDaqdz6ucP2RKHsBjEqaUA2ebNv2SsZoPEi+ZD0dZbOB1u37tGCA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.12", + "@smithy/protocol-http": "^4.1.8", + "@smithy/service-error-classification": "^3.0.11", + "@smithy/smithy-client": "^3.7.0", + "@smithy/types": "^3.7.2", + "@smithy/util-middleware": "^3.0.11", + "@smithy/util-retry": "^3.0.11", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-ecs/node_modules/@smithy/node-http-handler": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.3.3.tgz", + "integrity": "sha512-BrpZOaZ4RCbcJ2igiSNG16S+kgAc65l/2hmxWdmhyoGWHTLlzQzr06PXavJp9OBlPEG/sHlqdxjWmjzV66+BSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^3.1.9", + "@smithy/protocol-http": "^4.1.8", + "@smithy/querystring-builder": "^3.0.11", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-ecs/node_modules/@smithy/protocol-http": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.8.tgz", + "integrity": "sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-ecs/node_modules/@smithy/service-error-classification": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.11.tgz", + "integrity": "sha512-QnYDPkyewrJzCyaeI2Rmp7pDwbUETe+hU8ADkXmgNusO1bgHBH7ovXJiYmba8t0fNfJx75fE8dlM6SEmZxheog==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-ecs/node_modules/@smithy/util-retry": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.11.tgz", + "integrity": "sha512-hJUC6W7A3DQgaee3Hp9ZFcOxVDZzmBIRBPlUAk8/fSOEl7pE/aX7Dci0JycNOnm9Mfr0KV2XjIlUOcGWXQUdVQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^3.0.11", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-schemas": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-schemas/-/client-schemas-3.693.0.tgz", + "integrity": "sha512-a6B9z2hBlO67c8k6WMJNhFP26VCYEaL7aAo3oe/IbT1sncD6cSoROF5L0o9ebsosA+81Xkkvjj2zeF/+ohdAng==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-stream": "^3.3.0", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.8", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-schemas/node_modules/@smithy/fetch-http-handler": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.3.tgz", + "integrity": "sha512-6SxNltSncI8s689nvnzZQc/dPXcpHQ34KUj6gR/HBroytKOd/isMG3gJF/zBE1TBmTT18TXyzhg3O3SOOqGEhA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.8", + "@smithy/querystring-builder": "^3.0.11", + "@smithy/types": "^3.7.2", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "packages/core/node_modules/@aws-sdk/client-schemas/node_modules/@smithy/middleware-retry": { + "version": "3.0.34", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.34.tgz", + "integrity": "sha512-yVRr/AAtPZlUvwEkrq7S3x7Z8/xCd97m2hLDaqdz6ucP2RKHsBjEqaUA2ebNv2SsZoPEi+ZD0dZbOB1u37tGCA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.12", + "@smithy/protocol-http": "^4.1.8", + "@smithy/service-error-classification": "^3.0.11", + "@smithy/smithy-client": "^3.7.0", + "@smithy/types": "^3.7.2", + "@smithy/util-middleware": "^3.0.11", + "@smithy/util-retry": "^3.0.11", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-schemas/node_modules/@smithy/node-http-handler": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.3.3.tgz", + "integrity": "sha512-BrpZOaZ4RCbcJ2igiSNG16S+kgAc65l/2hmxWdmhyoGWHTLlzQzr06PXavJp9OBlPEG/sHlqdxjWmjzV66+BSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^3.1.9", + "@smithy/protocol-http": "^4.1.8", + "@smithy/querystring-builder": "^3.0.11", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-schemas/node_modules/@smithy/protocol-http": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.8.tgz", + "integrity": "sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-schemas/node_modules/@smithy/service-error-classification": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.11.tgz", + "integrity": "sha512-QnYDPkyewrJzCyaeI2Rmp7pDwbUETe+hU8ADkXmgNusO1bgHBH7ovXJiYmba8t0fNfJx75fE8dlM6SEmZxheog==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-schemas/node_modules/@smithy/util-retry": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.11.tgz", + "integrity": "sha512-hJUC6W7A3DQgaee3Hp9ZFcOxVDZzmBIRBPlUAk8/fSOEl7pE/aX7Dci0JycNOnm9Mfr0KV2XjIlUOcGWXQUdVQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^3.0.11", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-secrets-manager": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.693.0.tgz", + "integrity": "sha512-PiXkl64LYhwZQ2zPQhxwpnLwGS7Lw8asFCj29SxEaYRnYra3ajE5d+Yvv68qC+diUNkeZh6k6zn7nEOZ4rWEwA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/fetch-http-handler": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.3.tgz", + "integrity": "sha512-6SxNltSncI8s689nvnzZQc/dPXcpHQ34KUj6gR/HBroytKOd/isMG3gJF/zBE1TBmTT18TXyzhg3O3SOOqGEhA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.8", + "@smithy/querystring-builder": "^3.0.11", + "@smithy/types": "^3.7.2", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "packages/core/node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/middleware-retry": { + "version": "3.0.34", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.34.tgz", + "integrity": "sha512-yVRr/AAtPZlUvwEkrq7S3x7Z8/xCd97m2hLDaqdz6ucP2RKHsBjEqaUA2ebNv2SsZoPEi+ZD0dZbOB1u37tGCA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.12", + "@smithy/protocol-http": "^4.1.8", + "@smithy/service-error-classification": "^3.0.11", + "@smithy/smithy-client": "^3.7.0", + "@smithy/types": "^3.7.2", + "@smithy/util-middleware": "^3.0.11", + "@smithy/util-retry": "^3.0.11", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/node-http-handler": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.3.3.tgz", + "integrity": "sha512-BrpZOaZ4RCbcJ2igiSNG16S+kgAc65l/2hmxWdmhyoGWHTLlzQzr06PXavJp9OBlPEG/sHlqdxjWmjzV66+BSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^3.1.9", + "@smithy/protocol-http": "^4.1.8", + "@smithy/querystring-builder": "^3.0.11", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/protocol-http": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.8.tgz", + "integrity": "sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/service-error-classification": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.11.tgz", + "integrity": "sha512-QnYDPkyewrJzCyaeI2Rmp7pDwbUETe+hU8ADkXmgNusO1bgHBH7ovXJiYmba8t0fNfJx75fE8dlM6SEmZxheog==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/core/node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-retry": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.11.tgz", + "integrity": "sha512-hJUC6W7A3DQgaee3Hp9ZFcOxVDZzmBIRBPlUAk8/fSOEl7pE/aX7Dci0JycNOnm9Mfr0KV2XjIlUOcGWXQUdVQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^3.0.11", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "packages/core/node_modules/@aws-sdk/client-sso": { "version": "3.693.0", "license": "Apache-2.0", @@ -32322,6 +44006,7 @@ "packages/core/node_modules/@aws-sdk/client-sts": { "version": "3.693.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -32844,6 +44529,99 @@ } } }, + "packages/core/node_modules/@aws/language-server-runtimes": { + "version": "0.2.129", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.129.tgz", + "integrity": "sha512-ZTObivXrC04FIZHlRgL/E3Dx+hq4wFMOXCGTMHlVUiRs8FaXLXvENZbi0+5/I3Ex/CNwazQWgVaBHJ+dMw42nw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws/language-server-runtimes-types": "^0.1.56", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.200.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.200.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.200.0", + "@opentelemetry/resources": "^2.0.1", + "@opentelemetry/sdk-logs": "^0.200.0", + "@opentelemetry/sdk-metrics": "^2.0.1", + "@smithy/node-http-handler": "^4.0.4", + "ajv": "^8.17.1", + "aws-sdk": "^2.1692.0", + "hpagent": "^1.2.0", + "jose": "^5.9.6", + "mac-ca": "^3.1.1", + "registry-js": "^1.16.1", + "rxjs": "^7.8.2", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-uri": "^3.1.0", + "win-ca": "^3.5.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/core/node_modules/@aws/language-server-runtimes/node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "packages/core/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "packages/core/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "packages/core/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "packages/core/node_modules/@smithy/fetch-http-handler": { "version": "5.0.2", "license": "Apache-2.0", @@ -33351,16 +45129,44 @@ "dev": true, "license": "MIT" }, + "packages/core/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "packages/core/node_modules/brace-expansion": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, + "packages/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "packages/core/node_modules/minimatch": { "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "license": "ISC", "dependencies": { @@ -33372,6 +45178,8 @@ }, "packages/core/node_modules/mocha": { "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, "license": "MIT", "dependencies": { @@ -33406,6 +45214,9 @@ }, "packages/core/node_modules/mocha/node_modules/glob": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -33424,11 +45235,15 @@ }, "packages/core/node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "packages/core/node_modules/supports-color": { "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -33441,13 +45256,24 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "packages/core/node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, "packages/core/node_modules/workerpool": { "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true, "license": "Apache-2.0" }, "packages/core/node_modules/yargs": { "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "license": "MIT", "dependencies": { @@ -33479,7 +45305,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.72.0-SNAPSHOT", + "version": "3.91.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -33494,6 +45320,7 @@ "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { + "@types/eslint": "^8.56.0", "mocha": "^10.1.0" }, "engines": { diff --git a/package.json b/package.json index 0647493580b..5dd78f624ff 100644 --- a/package.json +++ b/package.json @@ -42,10 +42,11 @@ "reset": "npm run clean && ts-node ./scripts/clean.ts node_modules && npm install", "generateNonCodeFiles": "npm run generateNonCodeFiles -w packages/ --if-present", "mergeReports": "ts-node ./scripts/mergeReports.ts", - "skippedTestReport": "ts-node ./scripts/skippedTestReport.ts ./packages/amazonq/test/e2e/" + "skippedTestReport": "ts-node ./scripts/skippedTestReport.ts ./packages/amazonq/test/e2e/", + "scan-licenses": "ts-node ./scripts/scan-licenses.ts" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.329", + "@aws-toolkits/telemetry": "^1.0.338", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", @@ -82,6 +83,7 @@ "webpack-merge": "^5.10.0" }, "dependencies": { + "@aws/language-server-runtimes": "^0.3.5", "@types/node": "^22.7.5", "@types/selenium-webdriver": "^4.1.28", "buffer": "^6.0.3", diff --git a/packages/amazonq/.changes/1.100.0.json b/packages/amazonq/.changes/1.100.0.json new file mode 100644 index 00000000000..e1deb61908b --- /dev/null +++ b/packages/amazonq/.changes/1.100.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-10-16", + "version": "1.100.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.101.0.json b/packages/amazonq/.changes/1.101.0.json new file mode 100644 index 00000000000..7a72dabfc9e --- /dev/null +++ b/packages/amazonq/.changes/1.101.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-10-22", + "version": "1.101.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.102.0.json b/packages/amazonq/.changes/1.102.0.json new file mode 100644 index 00000000000..df8ee166397 --- /dev/null +++ b/packages/amazonq/.changes/1.102.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-10-30", + "version": "1.102.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.103.0.json b/packages/amazonq/.changes/1.103.0.json new file mode 100644 index 00000000000..b7ba187c759 --- /dev/null +++ b/packages/amazonq/.changes/1.103.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-11-06", + "version": "1.103.0", + "entries": [ + { + "type": "Feature", + "description": "Q CodeTransformation: add more job metadata to history table" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.104.0.json b/packages/amazonq/.changes/1.104.0.json new file mode 100644 index 00000000000..d6346984469 --- /dev/null +++ b/packages/amazonq/.changes/1.104.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-11-15", + "version": "1.104.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.105.0.json b/packages/amazonq/.changes/1.105.0.json new file mode 100644 index 00000000000..1a435ce46b9 --- /dev/null +++ b/packages/amazonq/.changes/1.105.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-11-19", + "version": "1.105.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Remove show logs menu item for non Q views" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.106.0.json b/packages/amazonq/.changes/1.106.0.json new file mode 100644 index 00000000000..23dffe4c5e6 --- /dev/null +++ b/packages/amazonq/.changes/1.106.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-11-21", + "version": "1.106.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.89.0.json b/packages/amazonq/.changes/1.89.0.json new file mode 100644 index 00000000000..95ef52909d5 --- /dev/null +++ b/packages/amazonq/.changes/1.89.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-08-13", + "version": "1.89.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.90.0.json b/packages/amazonq/.changes/1.90.0.json new file mode 100644 index 00000000000..547528bce40 --- /dev/null +++ b/packages/amazonq/.changes/1.90.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-08-15", + "version": "1.90.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.91.0.json b/packages/amazonq/.changes/1.91.0.json new file mode 100644 index 00000000000..b555f97447c --- /dev/null +++ b/packages/amazonq/.changes/1.91.0.json @@ -0,0 +1,14 @@ +{ + "date": "2025-08-22", + "version": "1.91.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Enable inline completion in Jupyter Notebook" + }, + { + "type": "Feature", + "description": "Amazon Q supports admin control for MCP servers to restrict MCP server usage" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.92.0.json b/packages/amazonq/.changes/1.92.0.json new file mode 100644 index 00000000000..46f2518fb37 --- /dev/null +++ b/packages/amazonq/.changes/1.92.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-08-28", + "version": "1.92.0", + "entries": [ + { + "type": "Feature", + "description": "Amazon Q supports admin control for MCP servers to restrict MCP server usage" + }, + { + "type": "Feature", + "description": "Enabling dynamic model fetching capabilities in Amazon Q chat" + }, + { + "type": "Feature", + "description": "Amazon Q: Support for configuring and utilizing remote MCP servers." + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.93.0.json b/packages/amazonq/.changes/1.93.0.json new file mode 100644 index 00000000000..c8f34a95645 --- /dev/null +++ b/packages/amazonq/.changes/1.93.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-09-05", + "version": "1.93.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.94.0.json b/packages/amazonq/.changes/1.94.0.json new file mode 100644 index 00000000000..d0adc1ee037 --- /dev/null +++ b/packages/amazonq/.changes/1.94.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-09-11", + "version": "1.94.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.95.0.json b/packages/amazonq/.changes/1.95.0.json new file mode 100644 index 00000000000..8014b9e23b2 --- /dev/null +++ b/packages/amazonq/.changes/1.95.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-09-19", + "version": "1.95.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Amazon Q automatically refreshes expired IAM Credentials in Sagemaker instances" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.96.0.json b/packages/amazonq/.changes/1.96.0.json new file mode 100644 index 00000000000..17919dd6374 --- /dev/null +++ b/packages/amazonq/.changes/1.96.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-09-25", + "version": "1.96.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Amazon Q support web/container environments running Ubuntu/Linux, even when the host machine is Amazon Linux 2." + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.97.0.json b/packages/amazonq/.changes/1.97.0.json new file mode 100644 index 00000000000..94952817128 --- /dev/null +++ b/packages/amazonq/.changes/1.97.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-09-29", + "version": "1.97.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.98.0.json b/packages/amazonq/.changes/1.98.0.json new file mode 100644 index 00000000000..a71130bc08a --- /dev/null +++ b/packages/amazonq/.changes/1.98.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-10-02", + "version": "1.98.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.99.0.json b/packages/amazonq/.changes/1.99.0.json new file mode 100644 index 00000000000..9d1089ee8fa --- /dev/null +++ b/packages/amazonq/.changes/1.99.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-10-10", + "version": "1.99.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index 806a99a319e..a50dbf849ab 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,78 @@ +## 1.106.0 2025-11-21 + +- Miscellaneous non-user-facing changes + +## 1.105.0 2025-11-19 + +- **Bug Fix** Remove show logs menu item for non Q views + +## 1.104.0 2025-11-15 + +- Miscellaneous non-user-facing changes + +## 1.103.0 2025-11-06 + +- **Feature** Q CodeTransformation: add more job metadata to history table + +## 1.102.0 2025-10-30 + +- Miscellaneous non-user-facing changes + +## 1.101.0 2025-10-22 + +- Miscellaneous non-user-facing changes + +## 1.100.0 2025-10-16 + +- Miscellaneous non-user-facing changes + +## 1.99.0 2025-10-10 + +- Miscellaneous non-user-facing changes + +## 1.98.0 2025-10-02 + +- Miscellaneous non-user-facing changes + +## 1.97.0 2025-09-29 + +- Miscellaneous non-user-facing changes + +## 1.96.0 2025-09-25 + +- **Bug Fix** Amazon Q support web/container environments running Ubuntu/Linux, even when the host machine is Amazon Linux 2. + +## 1.95.0 2025-09-19 + +- **Bug Fix** Amazon Q automatically refreshes expired IAM Credentials in Sagemaker instances + +## 1.94.0 2025-09-11 + +- Miscellaneous non-user-facing changes + +## 1.93.0 2025-09-05 + +- Miscellaneous non-user-facing changes + +## 1.92.0 2025-08-28 + +- **Feature** Amazon Q supports admin control for MCP servers to restrict MCP server usage +- **Feature** Enabling dynamic model fetching capabilities in Amazon Q chat +- **Feature** Amazon Q: Support for configuring and utilizing remote MCP servers. + +## 1.91.0 2025-08-22 + +- **Bug Fix** Enable inline completion in Jupyter Notebook +- **Feature** Amazon Q supports admin control for MCP servers to restrict MCP server usage + +## 1.90.0 2025-08-15 + +- Miscellaneous non-user-facing changes + +## 1.89.0 2025-08-13 + +- Miscellaneous non-user-facing changes + ## 1.88.0 2025-08-06 - **Feature** Amazon Q Chat provides error explanations and fixes when hovering or right-clicking on error indicators and messages diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 428b641263e..5af8a0e85bc 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -2,7 +2,7 @@ "name": "amazon-q-vscode", "displayName": "Amazon Q", "description": "The most capable generative AI–powered assistant for software development.", - "version": "1.89.0-SNAPSHOT", + "version": "1.107.0-SNAPSHOT", "extensionKind": [ "workspace" ], @@ -219,6 +219,11 @@ "markdownDescription": "%AWS.configuration.description.amazonq.proxy.certificateAuthority%", "default": null, "scope": "application" + }, + "amazonQ.proxy.enableProxyAndCertificateAutoDiscovery": { + "type": "boolean", + "markdownDescription": "%AWS.configuration.description.amazonq.proxy.enableProxyAndCertificateAutoDiscovery%", + "default": true } } }, @@ -410,7 +415,7 @@ }, { "command": "aws.amazonq.showLogs", - "when": "!aws.isSageMakerUnifiedStudio", + "when": "(view =~ /^aws\\.amazonq/) && !aws.isSageMakerUnifiedStudio", "group": "1_amazonQ@5" }, { @@ -588,12 +593,6 @@ "category": "%AWS.amazonq.title%", "enablement": "aws.codewhisperer.connected" }, - { - "command": "aws.amazonq.security.scan-statusbar", - "title": "%AWS.command.amazonq.security.scan%", - "category": "%AWS.amazonq.title%", - "enablement": "aws.codewhisperer.connected && !aws.isSageMaker" - }, { "command": "aws.amazonq.refactorCode", "title": "%AWS.command.amazonq.refactorCode%", @@ -920,7 +919,7 @@ }, { "command": "aws.amazonq.fixCode", - "win": "win+alt+y", + "win": "win+alt+h", "mac": "cmd+alt+y", "linux": "meta+alt+y" }, @@ -938,7 +937,7 @@ }, { "command": "aws.amazonq.generateUnitTests", - "key": "win+alt+t", + "key": "win+alt+n", "mac": "cmd+alt+t", "linux": "meta+alt+t" }, @@ -1290,124 +1289,173 @@ "fontCharacter": "\\f1d2" } }, - "aws-lambda-function": { + "aws-lambda-deployed-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d3" } }, - "aws-mynah-MynahIconBlack": { + "aws-lambda-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d4" } }, - "aws-mynah-MynahIconWhite": { + "aws-lambda-invoke-remotely": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d5" } }, - "aws-mynah-logo": { + "aws-mynah-MynahIconBlack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d6" } }, - "aws-redshift-cluster": { + "aws-mynah-MynahIconWhite": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d7" } }, - "aws-redshift-cluster-connected": { + "aws-mynah-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d8" } }, - "aws-redshift-database": { + "aws-redshift-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d9" } }, - "aws-redshift-redshift-cluster-connected": { + "aws-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1da" } }, - "aws-redshift-schema": { + "aws-redshift-database": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1db" } }, - "aws-redshift-table": { + "aws-redshift-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1dc" } }, - "aws-s3-bucket": { + "aws-redshift-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1dd" } }, - "aws-s3-create-bucket": { + "aws-redshift-table": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1de" } }, - "aws-sagemaker-code-editor": { + "aws-s3-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1df" } }, - "aws-sagemaker-jupyter-lab": { + "aws-s3-create-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e0" } }, - "aws-schemas-registry": { + "aws-sagemaker-code-editor": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e1" } }, - "aws-schemas-schema": { + "aws-sagemaker-jupyter-lab": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e2" } }, - "aws-stepfunctions-preview": { + "aws-sagemakerunifiedstudio-catalog": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e3" } + }, + "aws-sagemakerunifiedstudio-spaces": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e4" + } + }, + "aws-sagemakerunifiedstudio-spaces-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e5" + } + }, + "aws-sagemakerunifiedstudio-symbol-int": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e6" + } + }, + "aws-sagemakerunifiedstudio-table": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e7" + } + }, + "aws-schemas-registry": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e8" + } + }, + "aws-schemas-schema": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e9" + } + }, + "aws-stepfunctions-preview": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1ea" + } } }, "walkthroughs": [ @@ -1436,17 +1484,6 @@ }, "completionEvents": [] }, - { - "id": "aws.amazonq.walkthrough.securityScan", - "title": "Check for security vulnerabilities", - "description": "Amazon Q scans your code to identify security vulnerabilities and suggests fixes.\n\nStart a scan from the status bar menu.\n\n[Scan your current project](command:_aws.amazonq.walkthrough.securityScanExample)\n\nIdentifies vulnerabilities in Python, Typescript, Ruby, AWS CDK, and [more](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-language-ide-support.html#security-scans-language-support)", - "media": { - "markdown": "./resources/walkthrough/amazonq/scans.md" - }, - "completionEvents": [ - "onCommand:_aws.amazonq.walkthrough.securityScanExample" - ] - }, { "id": "aws.amazonq.walkthrough.settings", "title": "Access actions and options", diff --git a/packages/amazonq/src/app/amazonqScan/app.ts b/packages/amazonq/src/app/amazonqScan/app.ts index 2b237ab534e..bd12e3acd01 100644 --- a/packages/amazonq/src/app/amazonqScan/app.ts +++ b/packages/amazonq/src/app/amazonqScan/app.ts @@ -4,13 +4,7 @@ */ import * as vscode from 'vscode' -import { - AmazonQAppInitContext, - MessagePublisher, - MessageListener, - focusAmazonQPanel, - DefaultAmazonQAppInitContext, -} from 'aws-core-vscode/amazonq' +import { AmazonQAppInitContext, MessageListener } from 'aws-core-vscode/amazonq' import { AuthUtil, codeScanState, onDemandFileScanState } from 'aws-core-vscode/codewhisperer' import { ScanChatControllerEventEmitters, ChatSessionManager } from 'aws-core-vscode/amazonqScan' import { ScanController } from './chat/controller/controller' @@ -18,8 +12,6 @@ import { AppToWebViewMessageDispatcher } from './chat/views/connector/connector' import { Messenger } from './chat/controller/messenger/messenger' import { UIMessageListener } from './chat/views/actions/uiMessageListener' import { debounce } from 'lodash' -import { Commands, placeholder } from 'aws-core-vscode/shared' -import { codeReviewInChat } from './models/constants' export function init(appContext: AmazonQAppInitContext) { const scanChatControllerEventEmitters: ScanChatControllerEventEmitters = { @@ -50,8 +42,6 @@ export function init(appContext: AmazonQAppInitContext) { webViewMessageListener: new MessageListener(scanChatUIInputEventEmitter), }) - appContext.registerWebViewToAppMessagePublisher(new MessagePublisher(scanChatUIInputEventEmitter), 'review') - const debouncedEvent = debounce(async () => { const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' let authenticatingSessionID = '' @@ -75,20 +65,6 @@ export function init(appContext: AmazonQAppInitContext) { return debouncedEvent() }) - if (!codeReviewInChat) { - Commands.register('aws.amazonq.security.scan-statusbar', async () => { - if (AuthUtil.instance.isConnectionExpired()) { - await AuthUtil.instance.notifyReauthenticate() - } - return focusAmazonQPanel.execute(placeholder, 'amazonq.security.scan').then(() => { - DefaultAmazonQAppInitContext.instance.getAppsToWebViewMessagePublisher().publish({ - sender: 'amazonqCore', - command: 'review', - }) - }) - }) - } - codeScanState.setChatControllers(scanChatControllerEventEmitters) onDemandFileScanState.setChatControllers(scanChatControllerEventEmitters) } diff --git a/packages/amazonq/src/app/chat/activation.ts b/packages/amazonq/src/app/chat/activation.ts index 659115d4256..7517d668497 100644 --- a/packages/amazonq/src/app/chat/activation.ts +++ b/packages/amazonq/src/app/chat/activation.ts @@ -17,7 +17,6 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push( amazonq.focusAmazonQChatWalkthrough.register(), amazonq.walkthroughInlineSuggestionsExample.register(), - amazonq.walkthroughSecurityScanExample.register(), amazonq.openAmazonQWalkthrough.register(), amazonq.listCodeWhispererCommandsWalkthrough.register(), amazonq.focusAmazonQPanel.register(), diff --git a/packages/amazonq/src/app/inline/EditRendering/displayImage.ts b/packages/amazonq/src/app/inline/EditRendering/displayImage.ts index f25284c6b5a..df4841bacfd 100644 --- a/packages/amazonq/src/app/inline/EditRendering/displayImage.ts +++ b/packages/amazonq/src/app/inline/EditRendering/displayImage.ts @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { getContext, getLogger, setContext } from 'aws-core-vscode/shared' +import { getLogger, setContext } from 'aws-core-vscode/shared' import * as vscode from 'vscode' import { applyPatch, diffLines } from 'diff' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { CodeWhispererSession } from '../sessionManager' import { LogInlineCompletionSessionResultsParams } from '@aws/language-server-runtimes/protocol' import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol' @@ -16,7 +16,7 @@ import { EditSuggestionState } from '../editSuggestionState' import type { AmazonQInlineCompletionItemProvider } from '../completion' import { vsCodeState } from 'aws-core-vscode/codewhisperer' -const autoRejectEditCursorDistance = 25 +const autoDiscardEditCursorDistance = 10 export class EditDecorationManager { private imageDecorationType: vscode.TextEditorDecorationType @@ -24,7 +24,7 @@ export class EditDecorationManager { private currentImageDecoration: vscode.DecorationOptions | undefined private currentRemovedCodeDecorations: vscode.DecorationOptions[] = [] private acceptHandler: (() => void) | undefined - private rejectHandler: (() => void) | undefined + private rejectHandler: ((isDiscard: boolean) => void) | undefined constructor() { this.registerCommandHandlers() @@ -131,15 +131,16 @@ export class EditDecorationManager { svgImage: vscode.Uri, startLine: number, onAccept: () => Promise, - onReject: () => Promise, + onReject: (isDiscard: boolean) => Promise, originalCode: string, newCode: string, originalCodeHighlightRanges: Array<{ line: number; start: number; end: number }> ): Promise { - await this.clearDecorations(editor) - - await setContext('aws.amazonq.editSuggestionActive' as any, true) - EditSuggestionState.setEditSuggestionActive(true) + // Clear old decorations but don't reset state (state is already set in displaySvgDecoration) + editor.setDecorations(this.imageDecorationType, []) + editor.setDecorations(this.removedCodeDecorationType, []) + this.currentImageDecoration = undefined + this.currentRemovedCodeDecorations = [] this.acceptHandler = onAccept this.rejectHandler = onReject @@ -162,7 +163,10 @@ export class EditDecorationManager { /** * Clears all edit suggestion decorations */ - public async clearDecorations(editor: vscode.TextEditor): Promise { + public async clearDecorations(editor: vscode.TextEditor, disposables: vscode.Disposable[]): Promise { + for (const d of disposables) { + d.dispose() + } editor.setDecorations(this.imageDecorationType, []) editor.setDecorations(this.removedCodeDecorationType, []) this.currentImageDecoration = undefined @@ -185,9 +189,9 @@ export class EditDecorationManager { }) // Register Esc key handler for rejecting suggestion - vscode.commands.registerCommand('aws.amazonq.inline.rejectEdit', () => { + vscode.commands.registerCommand('aws.amazonq.inline.rejectEdit', (isDiscard: boolean = false) => { if (this.rejectHandler) { - this.rejectHandler() + this.rejectHandler(isDiscard) } }) } @@ -307,60 +311,58 @@ export async function displaySvgDecoration( newCode: string, originalCodeHighlightRanges: Array<{ line: number; start: number; end: number }>, session: CodeWhispererSession, - languageClient: LanguageClient, + languageClient: BaseLanguageClient, item: InlineCompletionItemWithReferences, + listeners: vscode.Disposable[], inlineCompletionProvider?: AmazonQInlineCompletionItemProvider ) { + function logSuggestionFailure(type: 'DISCARD' | 'REJECT', reason: string, suggestionContent: string) { + getLogger('nextEditPrediction').debug( + `Auto ${type} edit suggestion with reason=${reason}, suggetion: ${suggestionContent}` + ) + } + // Check if edit is too far from current cursor position + const currentCursorLine = editor.selection.active.line + if (Math.abs(startLine - currentCursorLine) >= autoDiscardEditCursorDistance) { + // Emit DISCARD telemetry for edit suggestion that can't be shown because the suggestion is too far away + const params = createDiscardTelemetryParams(session, item) + void languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + logSuggestionFailure('DISCARD', 'cursor is too far away', item.insertText as string) + return + } + const originalCode = editor.document.getText() + // Set edit state immediately to prevent race condition with completion requests + await setContext('aws.amazonq.editSuggestionActive' as any, true) + EditSuggestionState.setEditSuggestionActive(true) + // Check if a completion suggestion is currently active - if so, discard edit suggestion if (inlineCompletionProvider && (await inlineCompletionProvider.isCompletionActive())) { + // Clean up state since we're not showing the edit + await setContext('aws.amazonq.editSuggestionActive' as any, false) + EditSuggestionState.setEditSuggestionActive(false) + // Emit DISCARD telemetry for edit suggestion that can't be shown due to active completion const params = createDiscardTelemetryParams(session, item) - languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) - getLogger().info('Edit suggestion discarded due to active completion suggestion') + void languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + logSuggestionFailure('DISCARD', 'Conflicting active inline completion', item.insertText as string) return } const isPatchValid = applyPatch(editor.document.getText(), item.insertText as string) if (!isPatchValid) { + // Clean up state since we're not showing the edit + await setContext('aws.amazonq.editSuggestionActive' as any, false) + EditSuggestionState.setEditSuggestionActive(false) + const params = createDiscardTelemetryParams(session, item) // TODO: this session is closed on flare side hence discarded is not emitted in flare - languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + void languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + logSuggestionFailure('DISCARD', 'Invalid patch', item.insertText as string) return } - const documentChangeListener = vscode.workspace.onDidChangeTextDocument((e) => { - if (e.contentChanges.length <= 0) { - return - } - if (e.document !== editor.document) { - return - } - if (vsCodeState.isCodeWhispererEditing) { - return - } - if (getContext('aws.amazonq.editSuggestionActive') === false) { - return - } - const isPatchValid = applyPatch(e.document.getText(), item.insertText as string) - if (!isPatchValid) { - void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit') - } - }) - const cursorChangeListener = vscode.window.onDidChangeTextEditorSelection((e) => { - if (!EditSuggestionState.isEditSuggestionActive()) { - return - } - if (e.textEditor !== editor) { - return - } - const currentPosition = e.selections[0].active - const distance = Math.abs(currentPosition.line - startLine) - if (distance > autoRejectEditCursorDistance) { - void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit') - } - }) await decorationManager.displayEditSuggestion( editor, svgImage, @@ -381,12 +383,8 @@ export async function displaySvgDecoration( const endPosition = getEndOfEditPosition(originalCode, newCode) editor.selection = new vscode.Selection(endPosition, endPosition) - // Move cursor to end of the actual changed content - editor.selection = new vscode.Selection(endPosition, endPosition) + await decorationManager.clearDecorations(editor, listeners) - await decorationManager.clearDecorations(editor) - documentChangeListener.dispose() - cursorChangeListener.dispose() const params: LogInlineCompletionSessionResultsParams = { sessionId: session.sessionId, completionSessionResult: { @@ -400,42 +398,39 @@ export async function displaySvgDecoration( firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, isInlineEdit: true, } - languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + void languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) session.triggerOnAcceptance = true - // VS Code triggers suggestion on every keystroke, temporarily disable trigger on acceptance - // if (inlineCompletionProvider && session.editsStreakPartialResultToken) { - // await inlineCompletionProvider.provideInlineCompletionItems( - // editor.document, - // endPosition, - // { - // triggerKind: vscode.InlineCompletionTriggerKind.Automatic, - // selectedCompletionInfo: undefined, - // }, - // new vscode.CancellationTokenSource().token, - // { emitTelemetry: false, showUi: false, editsStreakToken: session.editsStreakPartialResultToken } - // ) - // } }, - async () => { + async (isDiscard: boolean) => { // Handle reject - getLogger().info('Edit suggestion rejected') - await decorationManager.clearDecorations(editor) - documentChangeListener.dispose() - cursorChangeListener.dispose() + if (isDiscard) { + getLogger().info('Edit suggestion discarded') + } else { + getLogger().info('Edit suggestion rejected') + } + await decorationManager.clearDecorations(editor, listeners) + + const suggestionState = isDiscard + ? { + seen: false, + accepted: false, + discarded: true, + } + : { + seen: true, + accepted: false, + discarded: false, + } const params: LogInlineCompletionSessionResultsParams = { sessionId: session.sessionId, completionSessionResult: { - [item.itemId]: { - seen: true, - accepted: false, - discarded: false, - }, + [item.itemId]: suggestionState, }, totalSessionDisplayTime: Date.now() - session.requestStartTime, firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, isInlineEdit: true, } - languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + void languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) }, originalCode, newCode, diff --git a/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts b/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts index 6c52dc2d6a0..6a4eeacf642 100644 --- a/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts +++ b/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts @@ -4,56 +4,207 @@ */ import * as vscode from 'vscode' -import { displaySvgDecoration } from './displayImage' +import { displaySvgDecoration, decorationManager } from './displayImage' import { SvgGenerationService } from './svgGenerator' -import { getLogger } from 'aws-core-vscode/shared' -import { LanguageClient } from 'vscode-languageclient' +import { getContext, getLogger } from 'aws-core-vscode/shared' +import { BaseLanguageClient } from 'vscode-languageclient' import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol' import { CodeWhispererSession } from '../sessionManager' import type { AmazonQInlineCompletionItemProvider } from '../completion' +import { vsCodeState } from 'aws-core-vscode/codewhisperer' +import { applyPatch, createPatch } from 'diff' +import { EditSuggestionState } from '../editSuggestionState' +import { debounce } from 'aws-core-vscode/utils' -export async function showEdits( - item: InlineCompletionItemWithReferences, - editor: vscode.TextEditor | undefined, - session: CodeWhispererSession, - languageClient: LanguageClient, - inlineCompletionProvider?: AmazonQInlineCompletionItemProvider -) { - if (!editor) { - return - } - try { - const svgGenerationService = new SvgGenerationService() - // Generate your SVG image with the file contents - const currentFile = editor.document.uri.fsPath - const { svgImage, startLine, newCode, originalCodeHighlightRange } = await svgGenerationService.generateDiffSvg( - currentFile, - item.insertText as string - ) +const autoRejectEditCursorDistance = 25 +const maxPrefixRetryCharDiff = 5 +const rerenderDeboucneInMs = 500 + +enum RejectReason { + DocumentChange = 'Invalid patch due to document change', + NotApplicableToOriginal = 'ApplyPatch fail for original code', + MaxRetry = `Already retry ${maxPrefixRetryCharDiff} times`, +} + +export class EditsSuggestionSvg { + private readonly logger = getLogger('nextEditPrediction') + private documentChangedListener: vscode.Disposable | undefined + private cursorChangedListener: vscode.Disposable | undefined + + private startLine = 0 + + private documentChangeTrace = { + contentChanged: '', + count: 0, + } + + constructor( + private suggestion: InlineCompletionItemWithReferences, + private readonly editor: vscode.TextEditor, + private readonly languageClient: BaseLanguageClient, + private readonly session: CodeWhispererSession, + private readonly inlineCompletionProvider?: AmazonQInlineCompletionItemProvider + ) {} + + async show(patchedSuggestion?: InlineCompletionItemWithReferences) { + if (!this.editor) { + this.logger.error(`attempting to render an edit suggestion while editor is undefined`) + return + } + + const item = patchedSuggestion ? patchedSuggestion : this.suggestion - // TODO: To investigate why it fails and patch [generateDiffSvg] - if (newCode.length === 0) { - getLogger('nextEditPrediction').warn('not able to apply provided edit suggestion, skip rendering') + try { + const svgGenerationService = new SvgGenerationService() + // Generate your SVG image with the file contents + const currentFile = this.editor.document.uri.fsPath + const { svgImage, startLine, newCode, originalCodeHighlightRange } = + await svgGenerationService.generateDiffSvg(currentFile, this.suggestion.insertText as string) + + // For cursorChangeListener to access + this.startLine = startLine + + if (newCode.length === 0) { + this.logger.warn('not able to apply provided edit suggestion, skip rendering') + return + } + + if (svgImage) { + const documentChangedListener = (this.documentChangedListener ??= + vscode.workspace.onDidChangeTextDocument(async (e) => { + await this.onDocChange(e) + })) + + const cursorChangedListener = (this.cursorChangedListener ??= + vscode.window.onDidChangeTextEditorSelection((e) => { + this.onCursorChange(e) + })) + + // display the SVG image + await displaySvgDecoration( + this.editor, + svgImage, + startLine, + newCode, + originalCodeHighlightRange, + this.session, + this.languageClient, + item, + [documentChangedListener, cursorChangedListener], + this.inlineCompletionProvider + ) + } else { + this.logger.error('SVG image generation returned an empty result.') + } + } catch (error) { + this.logger.error(`Error generating SVG image: ${error}`) + } + } + + private onCursorChange(e: vscode.TextEditorSelectionChangeEvent) { + if (!EditSuggestionState.isEditSuggestionActive()) { + return + } + if (e.textEditor !== this.editor) { return } + const currentPosition = e.selections[0].active + const distance = Math.abs(currentPosition.line - this.startLine) + if (distance > autoRejectEditCursorDistance) { + this.autoReject(`cursor position move too far away off ${autoRejectEditCursorDistance} lines`) + } + } - if (svgImage) { - // display the SVG image - await displaySvgDecoration( - editor, - svgImage, - startLine, - newCode, - originalCodeHighlightRange, - session, - languageClient, - item, - inlineCompletionProvider + private async onDocChange(e: vscode.TextDocumentChangeEvent) { + if (e.contentChanges.length <= 0) { + return + } + if (e.document !== this.editor.document) { + return + } + if (vsCodeState.isCodeWhispererEditing) { + return + } + if (getContext('aws.amazonq.editSuggestionActive') === false) { + return + } + + // TODO: handle multi-contentChanges scenario + const diff = e.contentChanges[0] ? e.contentChanges[0].text : '' + this.logger.info(`docChange sessionId=${this.session.sessionId}, contentChange=${diff}`) + + // Track document changes because we might need to hide/reject suggestions while users are typing for better UX + this.documentChangeTrace.contentChanged += e.contentChanges[0].text + this.documentChangeTrace.count++ + /** + * 1. Take the diff returned by the model and apply it to the code we originally sent to the model + * 2. Do a diff between the above code and what's currently in the editor + * 3. Show this second diff to the user as the edit suggestion + */ + // Users' file content when the request fires (best guess because the actual process happens in language server) + const originalCode = this.session.fileContent + const appliedToOriginal = applyPatch(originalCode, this.suggestion.insertText as string) + try { + if (appliedToOriginal) { + const updatedPatch = this.patchSuggestion(appliedToOriginal) + + if ( + this.documentChangeTrace.contentChanged.length > maxPrefixRetryCharDiff || + this.documentChangeTrace.count > maxPrefixRetryCharDiff + ) { + // Reject the suggestion if users've typed over 5 characters while the suggestion is shown + this.autoReject(RejectReason.MaxRetry) + } else if (applyPatch(this.editor.document.getText(), updatedPatch.insertText as string) === false) { + this.autoReject(RejectReason.DocumentChange) + } else { + // Close the previoius popup and rerender it + this.logger.debug(`calling rerender with suggestion\n ${updatedPatch.insertText as string}`) + await this.debouncedRerender(updatedPatch) + } + } else { + this.autoReject(RejectReason.NotApplicableToOriginal) + } + } catch (e) { + this.logger.error(`encountered error while processing edit suggestion when users type ${e}`) + // TODO: Maybe we should auto reject/hide suggestions in this scenario + } + } + + async dispose() { + this.documentChangedListener?.dispose() + this.cursorChangedListener?.dispose() + await decorationManager.clearDecorations(this.editor, []) + } + + debouncedRerender = debounce( + async (suggestion: InlineCompletionItemWithReferences) => await this.rerender(suggestion), + rerenderDeboucneInMs, + true + ) + + private async rerender(suggestion: InlineCompletionItemWithReferences) { + await decorationManager.clearDecorations(this.editor, []) + await this.show(suggestion) + } + + private autoReject(reason: string) { + function logSuggestionFailure(type: 'REJECT', reason: string, suggestionContent: string) { + getLogger('nextEditPrediction').debug( + `Auto ${type} edit suggestion with reason=${reason}, suggetion: ${suggestionContent}` ) - } else { - getLogger('nextEditPrediction').error('SVG image generation returned an empty result.') } - } catch (error) { - getLogger('nextEditPrediction').error(`Error generating SVG image: ${error}`) + + logSuggestionFailure('REJECT', reason, this.suggestion.insertText as string) + void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit') + } + + private patchSuggestion(appliedToOriginal: string): InlineCompletionItemWithReferences { + const updatedPatch = createPatch( + this.editor.document.fileName, + this.editor.document.getText(), + appliedToOriginal + ) + this.logger.info(`Update edit suggestion\n ${updatedPatch}`) + return { ...this.suggestion, insertText: updatedPatch } } } diff --git a/packages/amazonq/src/app/inline/EditRendering/stringUtils.ts b/packages/amazonq/src/app/inline/EditRendering/stringUtils.ts new file mode 100644 index 00000000000..b8c9a52d052 --- /dev/null +++ b/packages/amazonq/src/app/inline/EditRendering/stringUtils.ts @@ -0,0 +1,28 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Strips common indentation from each line of code that may contain HTML tags + * @param lines Array of code lines (may contain HTML tags) + * @returns Array of code lines with common indentation removed + */ +export function stripCommonIndentation(lines: string[]): string[] { + if (lines.length === 0) { + return lines + } + const removeFirstTag = (line: string) => line.replace(/^<[^>]*>/, '') + const getLeadingWhitespace = (text: string) => text.match(/^\s*/)?.[0] || '' + + // Find minimum indentation across all lines + const minIndentLength = Math.min(...lines.map((line) => getLeadingWhitespace(removeFirstTag(line)).length)) + + // Remove common indentation from each line + return lines.map((line) => { + const firstTagRemovedLine = removeFirstTag(line) + const leadingWhitespace = getLeadingWhitespace(firstTagRemovedLine) + const reducedWhitespace = leadingWhitespace.substring(minIndentLength) + return line.replace(leadingWhitespace, reducedWhitespace) + }) +} diff --git a/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts b/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts index 6958be47f36..59752a7b08a 100644 --- a/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts +++ b/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts @@ -7,6 +7,7 @@ import { diffWordsWithSpace, diffLines } from 'diff' import * as vscode from 'vscode' import { ToolkitError, getLogger } from 'aws-core-vscode/shared' import { diffUtilities } from 'aws-core-vscode/shared' +import { stripCommonIndentation } from './stringUtils' type Range = { line: number; start: number; end: number } const logger = getLogger('nextEditPrediction') @@ -18,6 +19,8 @@ export const emptyDiffSvg = { originalCodeHighlightRange: [], } +const defaultLineHighlightLength = 4 + export class SvgGenerationService { /** * Generates an SVG image representing a code diff @@ -76,6 +79,7 @@ export class SvgGenerationService { const highlightRanges = this.generateHighlightRanges(removedLines, addedLines, modifiedLines) const diffAddedWithHighlight = this.getHighlightEdit(addedLines, highlightRanges.addedRanges) + const normalizedDiffLines = stripCommonIndentation(diffAddedWithHighlight) // Create SVG window, document, and container const window = createSVGWindow() @@ -88,7 +92,7 @@ export class SvgGenerationService { // Generate CSS for syntax highlighting HTML content based on theme const styles = this.generateStyles(currentTheme) - const htmlContent = this.generateHtmlContent(diffAddedWithHighlight, styles, offset) + const htmlContent = this.generateHtmlContent(normalizedDiffLines, styles, offset) // Create foreignObject to embed HTML const foreignObject = draw.foreignObject(width + offset, height) @@ -160,6 +164,9 @@ export class SvgGenerationService { white-space: pre-wrap; /* Preserve whitespace */ background-color: ${diffAdded}; } + .diff-unchanged { + white-space: pre-wrap; /* Preserve indentation for unchanged lines */ + } ` } @@ -227,7 +234,7 @@ export class SvgGenerationService { // If no ranges for this line, leave it as-is with HTML escaping if (lineRanges.length === 0) { - result.push(this.escapeHtml(line)) + result.push(`${this.escapeHtml(line)}`) continue } @@ -242,7 +249,7 @@ export class SvgGenerationService { // Add text before the current range (with HTML escaping) if (range.start > currentPos) { const beforeText = line.substring(currentPos, range.start) - highlightedLine += this.escapeHtml(beforeText) + highlightedLine += `${this.escapeHtml(beforeText)}` } // Add the highlighted part (with HTML escaping) @@ -256,7 +263,7 @@ export class SvgGenerationService { // Add any remaining text after the last range (with HTML escaping) if (currentPos < line.length) { const afterText = line.substring(currentPos) - highlightedLine += this.escapeHtml(afterText) + highlightedLine += `${this.escapeHtml(afterText)}` } result.push(highlightedLine) @@ -431,8 +438,12 @@ export class SvgGenerationService { for (let lineIndex = 0; lineIndex < originalCode.length; lineIndex++) { const line = originalCode[lineIndex] + /** + * If [line] is an empty line or only contains whitespace char, [diffWordsWithSpace] will say it's not an "remove", i.e. [part.removed] will be undefined, + * therefore the deletion will not be highlighted. Thus fallback this scenario to highlight the entire line + */ // If line exists in modifiedLines as a key, process character diffs - if (Array.from(modifiedLines.keys()).includes(line)) { + if (Array.from(modifiedLines.keys()).includes(line) && line.trim().length > 0) { const modifiedLine = modifiedLines.get(line)! const changes = diffWordsWithSpace(line, modifiedLine) @@ -455,7 +466,7 @@ export class SvgGenerationService { originalRanges.push({ line: lineIndex, start: 0, - end: line.length, + end: line.length ?? defaultLineHighlightLength, }) } } diff --git a/packages/amazonq/src/app/inline/activation.ts b/packages/amazonq/src/app/inline/activation.ts index 867ae95d9b5..5a86d340c00 100644 --- a/packages/amazonq/src/app/inline/activation.ts +++ b/packages/amazonq/src/app/inline/activation.ts @@ -5,30 +5,73 @@ import vscode from 'vscode' import { + acceptSuggestion, AuthUtil, + CodeSuggestionsState, + CodeWhispererCodeCoverageTracker, CodeWhispererConstants, + CodeWhispererSettings, + ConfigurationEntry, + DefaultCodeWhispererClient, + invokeRecommendation, isInlineCompletionEnabled, + KeyStrokeHandler, + RecommendationHandler, runtimeLanguageContext, TelemetryHelper, UserWrittenCodeTracker, vsCodeState, } from 'aws-core-vscode/codewhisperer' -import { globals, sleep } from 'aws-core-vscode/shared' +import { Commands, getLogger, globals, sleep } from 'aws-core-vscode/shared' +import { BaseLanguageClient } from 'vscode-languageclient' -export async function activate() { - if (isInlineCompletionEnabled()) { - // Debugging purpose: only initialize NextEditPredictionPanel when development - // NextEditPredictionPanel.getInstance() +export async function activate(languageClient: BaseLanguageClient) { + const codewhispererSettings = CodeWhispererSettings.instance + const client = new DefaultCodeWhispererClient() + if (isInlineCompletionEnabled()) { await setSubscriptionsforInlineCompletion() await AuthUtil.instance.setVscodeContextProps() + RecommendationHandler.instance.setLanguageClient(languageClient) + } + + function getAutoTriggerStatus(): boolean { + return CodeSuggestionsState.instance.isSuggestionsEnabled() + } + + async function getConfigEntry(): Promise { + const isShowMethodsEnabled: boolean = + vscode.workspace.getConfiguration('editor').get('suggest.showMethods') || false + const isAutomatedTriggerEnabled: boolean = getAutoTriggerStatus() + const isManualTriggerEnabled: boolean = true + const isSuggestionsWithCodeReferencesEnabled = codewhispererSettings.isSuggestionsWithCodeReferencesEnabled() + + // TODO:remove isManualTriggerEnabled + return { + isShowMethodsEnabled, + isManualTriggerEnabled, + isAutomatedTriggerEnabled, + isSuggestionsWithCodeReferencesEnabled, + } } async function setSubscriptionsforInlineCompletion() { + RecommendationHandler.instance.subscribeSuggestionCommands() + /** * Automated trigger */ globals.context.subscriptions.push( + acceptSuggestion.register(globals.context), + vscode.window.onDidChangeActiveTextEditor(async (editor) => { + await RecommendationHandler.instance.onEditorChange() + }), + vscode.window.onDidChangeWindowState(async (e) => { + await RecommendationHandler.instance.onFocusChange() + }), + vscode.window.onDidChangeTextEditorSelection(async (e) => { + await RecommendationHandler.instance.onCursorChange(e) + }), vscode.workspace.onDidChangeTextDocument(async (e) => { const editor = vscode.window.activeTextEditor if (!editor) { @@ -41,6 +84,7 @@ export async function activate() { return } + CodeWhispererCodeCoverageTracker.getTracker(e.document.languageId)?.countTotalTokens(e) UserWrittenCodeTracker.instance.onTextDocumentChange(e) /** * Handle this keystroke event only when @@ -54,10 +98,10 @@ export async function activate() { if (vsCodeState.lastUserModificationTime) { TelemetryHelper.instance.setTimeSinceLastModification( - performance.now() - vsCodeState.lastUserModificationTime + Date.now() - vsCodeState.lastUserModificationTime ) } - vsCodeState.lastUserModificationTime = performance.now() + vsCodeState.lastUserModificationTime = Date.now() /** * Important: Doing this sleep(10) is to make sure * 1. this event is processed by vs code first @@ -65,6 +109,19 @@ export async function activate() { * Then this event can be processed by our code. */ await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) + if (!RecommendationHandler.instance.isSuggestionVisible()) { + await KeyStrokeHandler.instance.processKeyStroke(e, editor, client, await getConfigEntry()) + } + }), + // manual trigger + Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { + invokeRecommendation( + vscode.window.activeTextEditor as vscode.TextEditor, + client, + await getConfigEntry() + ).catch((e: Error) => { + getLogger().error('invokeRecommendation failed: %s', (e as Error).message) + }) }) ) } diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index 9a5f5522468..6642ea6a2cd 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -18,7 +18,7 @@ import { InlineCompletionTriggerKind, Range, } from 'vscode' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { InlineCompletionItemWithReferences, LogInlineCompletionSessionResultsParams, @@ -37,20 +37,21 @@ import { getDiagnosticsOfCurrentFile, toIdeDiagnostics, handleExtraBrackets, + InlineCompletionLoggingReason, } from 'aws-core-vscode/codewhisperer' import { LineTracker } from './stateTracker/lineTracker' import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation' import { TelemetryHelper } from './telemetryHelper' -import { Experiments, getLogger, sleep } from 'aws-core-vscode/shared' +import { Experiments, getContext, getLogger, sleep } from 'aws-core-vscode/shared' import { messageUtils } from 'aws-core-vscode/utils' -import { showEdits } from './EditRendering/imageRenderer' +import { EditsSuggestionSvg } from './EditRendering/imageRenderer' import { ICursorUpdateRecorder } from './cursorUpdateManager' import { DocumentEventListener } from './documentEventListener' export class InlineCompletionManager implements Disposable { private disposable: Disposable private inlineCompletionProvider: AmazonQInlineCompletionItemProvider - private languageClient: LanguageClient + private languageClient: BaseLanguageClient private sessionManager: SessionManager private recommendationService: RecommendationService private lineTracker: LineTracker @@ -60,7 +61,7 @@ export class InlineCompletionManager implements Disposable { private documentEventListener: DocumentEventListener constructor( - languageClient: LanguageClient, + languageClient: BaseLanguageClient, sessionManager: SessionManager, lineTracker: LineTracker, inlineTutorialAnnotation: InlineTutorialAnnotation, @@ -115,7 +116,7 @@ export class InlineCompletionManager implements Disposable { const startLine = position.line // TODO: also log the seen state for other suggestions in session // Calculate timing metrics before diagnostic delay - const totalSessionDisplayTime = performance.now() - requestStartTime + const totalSessionDisplayTime = Date.now() - requestStartTime await sleep(500) const diagnosticDiff = getDiagnosticsDifferences( this.sessionManager.getActiveSession()?.diagnosticsBeforeAccept, @@ -140,7 +141,7 @@ export class InlineCompletionManager implements Disposable { addedDiagnostics: diagnosticDiff.added.map((it) => toIdeDiagnostics(it)), removedDiagnostics: diagnosticDiff.removed.map((it) => toIdeDiagnostics(it)), } - this.languageClient.sendNotification(this.logSessionResultMessageName, params) + void this.languageClient.sendNotification(this.logSessionResultMessageName, params) this.disposable.dispose() this.disposable = languages.registerInlineCompletionItemProvider( CodeWhispererConstants.platformLanguageIds, @@ -175,7 +176,7 @@ export class InlineCompletionManager implements Disposable { return } const requestStartTime = session.requestStartTime - const totalSessionDisplayTime = performance.now() - requestStartTime + const totalSessionDisplayTime = Date.now() - requestStartTime await commands.executeCommand('editor.action.inlineSuggest.hide') // TODO: also log the seen state for other suggestions in session this.disposable.dispose() @@ -200,7 +201,7 @@ export class InlineCompletionManager implements Disposable { firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, totalSessionDisplayTime: totalSessionDisplayTime, } - this.languageClient.sendNotification(this.logSessionResultMessageName, params) + void this.languageClient.sendNotification(this.logSessionResultMessageName, params) // clear session manager states once rejected this.sessionManager.clear() } finally { @@ -213,8 +214,11 @@ export class InlineCompletionManager implements Disposable { export class AmazonQInlineCompletionItemProvider implements InlineCompletionItemProvider { private logger = getLogger() + private pendingRequest: Promise | undefined + private lastEdit: EditsSuggestionSvg | undefined + constructor( - private readonly languageClient: LanguageClient, + private readonly languageClient: BaseLanguageClient, private readonly recommendationService: RecommendationService, private readonly sessionManager: SessionManager, private readonly inlineTutorialAnnotation: InlineTutorialAnnotation, @@ -249,7 +253,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem // Use VS Code command to check if inline suggestion is actually visible on screen // This command only executes when inlineSuggestionVisible context is true await vscode.commands.executeCommand('aws.amazonq.checkInlineSuggestionVisibility') - const isInlineSuggestionVisible = performance.now() - session.lastVisibleTime < 50 + const isInlineSuggestionVisible = Date.now() - session.lastVisibleTime < 50 return isInlineSuggestionVisible } @@ -278,9 +282,9 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem sessionId: session.sessionId, completionSessionResult, firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, - totalSessionDisplayTime: performance.now() - session.requestStartTime, + totalSessionDisplayTime: Date.now() - session.requestStartTime, } - this.languageClient.sendNotification(this.logSessionResultMessageName, params) + void this.languageClient.sendNotification(this.logSessionResultMessageName, params) } } @@ -300,22 +304,63 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem options: JSON.stringify(getAllRecommendationsOptions), }) - // prevent concurrent API calls and write to shared state variables - if (vsCodeState.isRecommendationsActive) { - getLogger().info('Recommendations already active, returning empty') - return [] + // If there's already a pending request, wait for it to complete instead of starting a new one + // This prevents race conditions where multiple concurrent calls cause the later (empty) response + // to override the earlier (valid) response + if (this.pendingRequest) { + getLogger().info('Reusing pending inline completion request to avoid race condition') + try { + const result = await this.pendingRequest + // Check if THIS call's token was cancelled (not the original call's token) + if (token.isCancellationRequested) { + getLogger().info('Reused request completed but this call was cancelled') + return [] + } + return result + } catch (e) { + // If the pending request failed, continue with a new request + getLogger().info('Pending request failed, starting new request: %O', e) + } + } + + // Start a new request and track it + this.pendingRequest = this._provideInlineCompletionItemsImpl( + document, + position, + context, + token, + getAllRecommendationsOptions + ) + + try { + return await this.pendingRequest + } finally { + this.pendingRequest = undefined } + } + private async _provideInlineCompletionItemsImpl( + document: TextDocument, + position: Position, + context: InlineCompletionContext, + token: CancellationToken, + getAllRecommendationsOptions?: GetAllRecommendationsOptions + ): Promise { if (vsCodeState.isCodeWhispererEditing) { getLogger().info('Q is editing, returning empty') return [] } + // Make edit suggestion blocking + if (getContext('aws.amazonq.editSuggestionActive') === true) { + return [] + } + // there is a bug in VS Code, when hitting Enter, the context.triggerKind is Invoke (0) // when hitting other keystrokes, the context.triggerKind is Automatic (1) // we only mark option + C as manual trigger // this is a workaround since the inlineSuggest.trigger command take no params - const isAutoTrigger = performance.now() - vsCodeState.lastManualTriggerTime > 50 + const isAutoTrigger = Date.now() - vsCodeState.lastManualTriggerTime > 50 if (isAutoTrigger && !CodeSuggestionsState.instance.isSuggestionsEnabled()) { // return early when suggestions are disabled with auto trigger return [] @@ -324,9 +369,9 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem // yield event loop to let the document listen catch updates await sleep(1) - let logstr = `GenerateCompletion metadata:\\n` + let logstr = `GenerateCompletion activity:\\n` try { - const t0 = performance.now() + const t0 = Date.now() vsCodeState.isRecommendationsActive = true // handling previous session const prevSession = this.sessionManager.getActiveSession() @@ -371,7 +416,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem // re-use previous suggestions as long as new typed prefix matches if (prevItemMatchingPrefix.length > 0) { logstr += `- not call LSP and reuse previous suggestions that match user typed characters - - duration between trigger to completion suggestion is displayed ${performance.now() - t0}` + - duration between trigger to completion suggestion is displayed ${Date.now() - t0}` void this.checkWhetherInlineCompletionWasShown() return prevItemMatchingPrefix } @@ -386,11 +431,15 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem discarded: !prevSession.displayed, }, }, + reason: InlineCompletionLoggingReason.IMPLICIT_REJECT, firstCompletionDisplayLatency: prevSession.firstCompletionDisplayLatency, - totalSessionDisplayTime: performance.now() - prevSession.requestStartTime, + totalSessionDisplayTime: Date.now() - prevSession.requestStartTime, } - this.languageClient.sendNotification(this.logSessionResultMessageName, params) + void this.languageClient.sendNotification(this.logSessionResultMessageName, params) this.sessionManager.clear() + // Do not make auto trigger if user rejects a suggestion + // by typing characters that does not match + return [] } // tell the tutorial that completions has been triggered @@ -399,7 +448,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem TelemetryHelper.instance.setInvokeSuggestionStartTime() TelemetryHelper.instance.setTriggerType(context.triggerKind) - const t1 = performance.now() + const t1 = Date.now() await this.recommendationService.getAllRecommendations( this.languageClient, @@ -421,7 +470,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem // eslint-disable-next-line @typescript-eslint/no-base-to-string const itemLog = items[0] ? `${items[0].insertText.toString()}` : `no suggestion` - const t2 = performance.now() + const t2 = Date.now() logstr += `- number of suggestions: ${items.length} - sessionId: ${this.sessionManager.getActiveSession()?.sessionId} @@ -458,7 +507,7 @@ ${itemLog} }, }, } - this.languageClient.sendNotification(this.logSessionResultMessageName, params) + void this.languageClient.sendNotification(this.logSessionResultMessageName, params) this.sessionManager.clear() logstr += `- cursor moved behind trigger position. Discarding completion suggestion...` return [] @@ -471,7 +520,7 @@ ${itemLog} const lastDocumentChange = this.documentEventListener.getLastDocumentChangeEvent(document.uri.fsPath) if ( lastDocumentChange && - performance.now() - lastDocumentChange.timestamp < CodeWhispererConstants.inlineSuggestionShowDelay + Date.now() - lastDocumentChange.timestamp < CodeWhispererConstants.inlineSuggestionShowDelay ) { await sleep(CodeWhispererConstants.showRecommendationTimerPollPeriod) } else { @@ -488,8 +537,13 @@ ${itemLog} if (item.isInlineEdit) { // Check if Next Edit Prediction feature flag is enabled if (Experiments.instance.get('amazonqLSPNEP', true)) { - await showEdits(item, editor, session, this.languageClient, this) - logstr += `- duration between trigger to edits suggestion is displayed: ${performance.now() - t0}ms` + if (this.lastEdit) { + await this.lastEdit.dispose() + } + const e = new EditsSuggestionSvg(item, editor, this.languageClient, session, this) + await e.show() + this.lastEdit = e + logstr += `- duration between trigger to edits suggestion is displayed: ${Date.now() - t0}ms` } return [] } @@ -525,7 +579,7 @@ ${itemLog} }, }, } - this.languageClient.sendNotification(this.logSessionResultMessageName, params) + void this.languageClient.sendNotification(this.logSessionResultMessageName, params) this.sessionManager.clear() logstr += `- suggestion does not match user typeahead from insertion position. Discarding suggestion...` return [] @@ -533,7 +587,7 @@ ${itemLog} this.sessionManager.updateCodeReferenceAndImports() // suggestions returned here will be displayed on screen - logstr += `- duration between trigger to completion suggestion is displayed: ${performance.now() - t0}ms` + logstr += `- duration between trigger to completion suggestion is displayed: ${Date.now() - t0}ms` void this.checkWhetherInlineCompletionWasShown() return itemsMatchingTypeahead as InlineCompletionItem[] } catch (e) { diff --git a/packages/amazonq/src/app/inline/cursorUpdateManager.ts b/packages/amazonq/src/app/inline/cursorUpdateManager.ts index 3c0ad6f6add..e06285d1f32 100644 --- a/packages/amazonq/src/app/inline/cursorUpdateManager.ts +++ b/packages/amazonq/src/app/inline/cursorUpdateManager.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { getLogger } from 'aws-core-vscode/shared' import { globals } from 'aws-core-vscode/shared' import { AmazonQInlineCompletionItemProvider } from './completion' @@ -36,7 +36,7 @@ export class CursorUpdateManager implements vscode.Disposable, ICursorUpdateReco private autotriggerStateDisposable?: vscode.Disposable constructor( - private readonly languageClient: LanguageClient, + private readonly languageClient: BaseLanguageClient, private readonly inlineCompletionProvider?: AmazonQInlineCompletionItemProvider ) { // Listen for autotrigger state changes to enable/disable the timer diff --git a/packages/amazonq/src/app/inline/documentEventListener.ts b/packages/amazonq/src/app/inline/documentEventListener.ts index 36f65dc7331..7af22a3015a 100644 --- a/packages/amazonq/src/app/inline/documentEventListener.ts +++ b/packages/amazonq/src/app/inline/documentEventListener.ts @@ -20,7 +20,7 @@ export class DocumentEventListener { if (this.lastDocumentChangeEventMap.size > this._maxDocument) { this.lastDocumentChangeEventMap.clear() } - this.lastDocumentChangeEventMap.set(e.document.uri.fsPath, { event: e, timestamp: performance.now() }) + this.lastDocumentChangeEventMap.set(e.document.uri.fsPath, { event: e, timestamp: Date.now() }) // The VS Code provideInlineCompletionCallback may not trigger when Enter is pressed, especially in Python files // manually make this trigger. In case of duplicate, the provideInlineCompletionCallback is already debounced if (this.isEnter(e) && vscode.window.activeTextEditor) { @@ -37,7 +37,7 @@ export class DocumentEventListener { const eventTime = result.timestamp const isDelete = (event && event.contentChanges.length === 1 && event.contentChanges[0].text === '') || false - const timeDiff = Math.abs(performance.now() - eventTime) + const timeDiff = Math.abs(Date.now() - eventTime) return timeDiff < 500 && isDelete } return false diff --git a/packages/amazonq/src/app/inline/editSuggestionState.ts b/packages/amazonq/src/app/inline/editSuggestionState.ts index 66a9211bdcf..61e4aebd142 100644 --- a/packages/amazonq/src/app/inline/editSuggestionState.ts +++ b/packages/amazonq/src/app/inline/editSuggestionState.ts @@ -8,12 +8,20 @@ */ export class EditSuggestionState { private static isEditSuggestionCurrentlyActive = false + private static displayStartTime = Date.now() static setEditSuggestionActive(active: boolean): void { this.isEditSuggestionCurrentlyActive = active + if (active) { + this.displayStartTime = Date.now() + } } static isEditSuggestionActive(): boolean { return this.isEditSuggestionCurrentlyActive } + + static isEditSuggestionDisplayingOverOneSecond(): boolean { + return this.isEditSuggestionActive() && Date.now() - this.displayStartTime > 1000 + } } diff --git a/packages/amazonq/src/app/inline/notebookUtil.ts b/packages/amazonq/src/app/inline/notebookUtil.ts new file mode 100644 index 00000000000..928de1aad33 --- /dev/null +++ b/packages/amazonq/src/app/inline/notebookUtil.ts @@ -0,0 +1,98 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' + +import { CodeWhispererConstants, runtimeLanguageContext } from 'aws-core-vscode/codewhisperer' +import { InlineCompletionWithReferencesParams } from '@aws/language-server-runtimes/server-interface' + +function getEnclosingNotebook(document: vscode.TextDocument): vscode.NotebookDocument | undefined { + // For notebook cells, find the existing notebook with a cell that matches the current document. + return vscode.workspace.notebookDocuments.find( + (nb) => nb.notebookType === 'jupyter-notebook' && nb.getCells().some((cell) => cell.document === document) + ) +} + +export function getNotebookContext( + notebook: vscode.NotebookDocument, + document: vscode.TextDocument, + position: vscode.Position +) { + // Expand the context for a cell inside of a noteboo with whatever text fits from the preceding and subsequent cells + const allCells = notebook.getCells() + const cellIndex = allCells.findIndex((cell) => cell.document === document) + let caretLeftFileContext = '' + let caretRightFileContext = '' + + if (cellIndex >= 0 && cellIndex < allCells.length) { + // Add content from previous cells + for (let i = 0; i < cellIndex; i++) { + caretLeftFileContext += convertCellContent(allCells[i]) + '\n' + } + + // Add content from current cell up to cursor + caretLeftFileContext += allCells[cellIndex].document.getText( + new vscode.Range(new vscode.Position(0, 0), position) + ) + + // Add content from cursor to end of current cell + caretRightFileContext = allCells[cellIndex].document.getText( + new vscode.Range( + position, + allCells[cellIndex].document.positionAt(allCells[cellIndex].document.getText().length) + ) + ) + + // Add content from following cells + for (let i = cellIndex + 1; i < allCells.length; i++) { + caretRightFileContext += '\n' + convertCellContent(allCells[i]) + } + } + caretLeftFileContext = caretLeftFileContext.slice(-CodeWhispererConstants.charactersLimit) + caretRightFileContext = caretRightFileContext.slice(0, CodeWhispererConstants.charactersLimit) + return { caretLeftFileContext, caretRightFileContext } +} + +// Convert the markup cells into code with comments +export function convertCellContent(cell: vscode.NotebookCell) { + const cellText = cell.document.getText() + if (cell.kind === vscode.NotebookCellKind.Markup) { + const commentPrefix = runtimeLanguageContext.getSingleLineCommentPrefix( + runtimeLanguageContext.normalizeLanguage(cell.document.languageId) ?? cell.document.languageId + ) + if (commentPrefix === '') { + return cellText + } + return cell.document + .getText() + .split('\n') + .map((line) => `${commentPrefix}${line}`) + .join('\n') + } + return cellText +} + +export function extractFileContextInNotebooks( + document: vscode.TextDocument, + position: vscode.Position +): InlineCompletionWithReferencesParams['fileContextOverride'] | undefined { + let caretLeftFileContext = '' + let caretRightFileContext = '' + const languageName = runtimeLanguageContext.normalizeLanguage(document.languageId) ?? document.languageId + if (document.uri.scheme === 'vscode-notebook-cell') { + const notebook = getEnclosingNotebook(document) + if (notebook) { + ;({ caretLeftFileContext, caretRightFileContext } = getNotebookContext(notebook, document, position)) + return { + leftFileContent: caretLeftFileContext, + rightFileContent: caretRightFileContext, + filename: document.fileName, + fileUri: document.uri.toString(), + programmingLanguage: languageName, + } + } + } + return undefined +} diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index 55c08c820fe..52a039126dd 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -8,9 +8,10 @@ import { inlineCompletionWithReferencesRequestType, TextDocumentContentChangeEvent, editCompletionRequestType, + LogInlineCompletionSessionResultsParams, } from '@aws/language-server-runtimes/protocol' -import { CancellationToken, InlineCompletionContext, Position, TextDocument } from 'vscode' -import { LanguageClient } from 'vscode-languageclient' +import { CancellationToken, InlineCompletionContext, Position, TextDocument, commands } from 'vscode' +import { BaseLanguageClient } from 'vscode-languageclient' import { SessionManager } from './sessionManager' import { AuthUtil, @@ -24,6 +25,7 @@ import { getLogger } from 'aws-core-vscode/shared' import { DocumentEventListener } from './documentEventListener' import { getOpenFilesInWindow } from 'aws-core-vscode/utils' import { asyncCallWithTimeout } from '../../util/timeoutUtil' +import { extractFileContextInNotebooks } from './notebookUtil' import { EditSuggestionState } from './editSuggestionState' export interface GetAllRecommendationsOptions { @@ -33,6 +35,8 @@ export interface GetAllRecommendationsOptions { } export class RecommendationService { + private logger = getLogger() + constructor( private readonly sessionManager: SessionManager, private cursorUpdateRecorder?: ICursorUpdateRecorder @@ -45,7 +49,7 @@ export class RecommendationService { } async getRecommendationsWithTimeout( - languageClient: LanguageClient, + languageClient: BaseLanguageClient, request: InlineCompletionWithReferencesParams, token: CancellationToken ) { @@ -62,7 +66,7 @@ export class RecommendationService { } async getAllRecommendations( - languageClient: LanguageClient, + languageClient: BaseLanguageClient, document: TextDocument, position: Position, context: InlineCompletionContext, @@ -97,7 +101,10 @@ export class RecommendationService { if (options.editsStreakToken) { request = { ...request, partialResultToken: options.editsStreakToken } } - const requestStartTime = performance.now() + if (document.uri.scheme === 'vscode-notebook-cell') { + request.fileContextOverride = extractFileContextInNotebooks(document, position) + } + const requestStartTime = Date.now() const statusBar = CodeWhispererStatusBarManager.instance // Only track telemetry if enabled @@ -112,7 +119,7 @@ export class RecommendationService { } // Handle first request - getLogger().info('Sending inline completion request: %O', { + this.logger.info('Sending inline completion request: %O', { method: inlineCompletionWithReferencesRequestType.method, request: { textDocument: request.textDocument, @@ -121,7 +128,7 @@ export class RecommendationService { nextToken: request.partialResultToken, }, }) - const t0 = performance.now() + const t0 = Date.now() // Best effort estimate of deletion const isTriggerByDeletion = documentEventListener.isLastEventDeletion(document.uri.fsPath) @@ -169,9 +176,9 @@ export class RecommendationService { } } - getLogger().info('Received inline completion response from LSP: %O', { + this.logger.info('Received inline completion response from LSP: %O', { sessionId: result.sessionId, - latency: performance.now() - t0, + latency: Date.now() - t0, itemCount: result.items?.length || 0, items: result.items?.map((item) => ({ itemId: item.itemId, @@ -183,6 +190,44 @@ export class RecommendationService { })), }) + if (result.items.length > 0 && result.items[0].isInlineEdit === false) { + if (isTriggerByDeletion) { + this.logger.info(`Suggestions were discarded; reason: triggerByDeletion`) + return [] + } + // Completion will not be rendered if an edit suggestion has been active for longer than 1 second + if (EditSuggestionState.isEditSuggestionDisplayingOverOneSecond()) { + const session = this.sessionManager.getActiveSession() + if (!session) { + this.logger.error(`Suggestions were discarded; reason: undefined conflicting session`) + return [] + } + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: Object.fromEntries( + result.items.map((item) => [ + item.itemId, + { + seen: false, + accepted: false, + discarded: true, + }, + ]) + ), + } + void languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + this.sessionManager.clear() + this.logger.info( + 'Suggetions were discarded; reason: active edit suggestion displayed longer than 1 second' + ) + return [] + } else if (EditSuggestionState.isEditSuggestionActive()) { + // discard the current edit suggestion if its display time is less than 1 sec + await commands.executeCommand('aws.amazonq.inline.rejectEdit', true) + this.logger.info('Discarding active edit suggestion displaying less than 1 second') + } + } + TelemetryHelper.instance.setSdkApiCallEndTime() TelemetryHelper.instance.setSessionId(result.sessionId) if (result.items.length > 0 && result.items[0].itemId !== undefined) { @@ -190,12 +235,13 @@ export class RecommendationService { } TelemetryHelper.instance.setFirstSuggestionShowTime() - const firstCompletionDisplayLatency = performance.now() - requestStartTime + const firstCompletionDisplayLatency = Date.now() - requestStartTime this.sessionManager.startSession( result.sessionId, result.items, requestStartTime, position, + document, firstCompletionDisplayLatency ) @@ -203,11 +249,10 @@ export class RecommendationService { // TODO: question, is it possible that the first request returns empty suggestion but has non-empty next token? if (result.partialResultToken) { + let logstr = `found non null next token; ` if (!isInlineEdit) { // If the suggestion is COMPLETIONS and there are more results to fetch, handle them in the background - getLogger().info( - 'Suggestion type is COMPLETIONS. Start fetching for more items if partialResultToken exists.' - ) + logstr += 'Suggestion type is COMPLETIONS. Start pulling more items' this.processRemainingRequests(languageClient, request, result, token).catch((error) => { languageClient.warn(`Error when getting suggestions: ${error}`) }) @@ -215,12 +260,14 @@ export class RecommendationService { // Skip fetching for more items if the suggesion is EDITS. If it is EDITS suggestion, only fetching for more // suggestions when the user start to accept a suggesion. // Save editsStreakPartialResultToken for the next EDITS suggestion trigger if user accepts. - getLogger().info('Suggestion type is EDITS. Skip fetching for more items.') + logstr += 'Suggestion type is EDITS. Skip pulling more items' this.sessionManager.updateActiveEditsStreakToken(result.partialResultToken) } + + this.logger.info(logstr) } } catch (error: any) { - getLogger().error('Error getting recommendations: %O', error) + this.logger.error('Error getting recommendations: %O', error) // bearer token expired if (error.data && error.data.awsErrorCode === 'E_AMAZON_Q_CONNECTION_EXPIRED') { // ref: https://github.com/aws/aws-toolkit-vscode/blob/amazonq/v1.74.0/packages/core/src/codewhisperer/service/inlineCompletionService.ts#L104 @@ -243,7 +290,7 @@ export class RecommendationService { } private async processRemainingRequests( - languageClient: LanguageClient, + languageClient: BaseLanguageClient, initialRequest: InlineCompletionWithReferencesParams, firstResult: InlineCompletionListWithReferences, token: CancellationToken diff --git a/packages/amazonq/src/app/inline/sessionManager.ts b/packages/amazonq/src/app/inline/sessionManager.ts index 15d7dbbb8d0..85b83dd3997 100644 --- a/packages/amazonq/src/app/inline/sessionManager.ts +++ b/packages/amazonq/src/app/inline/sessionManager.ts @@ -28,6 +28,7 @@ export interface CodeWhispererSession { displayed: boolean // timestamp when the suggestion was last visible lastVisibleTime: number + fileContent: string } export class SessionManager { @@ -42,6 +43,7 @@ export class SessionManager { suggestions: InlineCompletionItemWithReferences[], requestStartTime: number, startPosition: vscode.Position, + document: vscode.TextDocument, firstCompletionDisplayLatency?: number ) { const diagnosticsBeforeAccept = getDiagnosticsOfCurrentFile() @@ -55,6 +57,7 @@ export class SessionManager { diagnosticsBeforeAccept, displayed: false, lastVisibleTime: 0, + fileContent: document.getText(), } this._currentSuggestionIndex = 0 } @@ -137,7 +140,7 @@ export class SessionManager { public checkInlineSuggestionVisibility() { if (this.activeSession) { this.activeSession.displayed = true - this.activeSession.lastVisibleTime = performance.now() + this.activeSession.lastVisibleTime = Date.now() } } diff --git a/packages/amazonq/src/app/inline/telemetryHelper.ts b/packages/amazonq/src/app/inline/telemetryHelper.ts index dffd267bee1..41db4c7469a 100644 --- a/packages/amazonq/src/app/inline/telemetryHelper.ts +++ b/packages/amazonq/src/app/inline/telemetryHelper.ts @@ -41,7 +41,7 @@ export class TelemetryHelper { public setInvokeSuggestionStartTime() { this.resetClientComponentLatencyTime() - this._invokeSuggestionStartTime = performance.now() + this._invokeSuggestionStartTime = Date.now() } get invokeSuggestionStartTime(): number { @@ -49,7 +49,7 @@ export class TelemetryHelper { } public setPreprocessEndTime() { - this._preprocessEndTime = performance.now() + this._preprocessEndTime = Date.now() } get preprocessEndTime(): number { @@ -58,7 +58,7 @@ export class TelemetryHelper { public setSdkApiCallStartTime() { if (this._sdkApiCallStartTime === 0) { - this._sdkApiCallStartTime = performance.now() + this._sdkApiCallStartTime = Date.now() } } @@ -68,7 +68,7 @@ export class TelemetryHelper { public setSdkApiCallEndTime() { if (this._sdkApiCallEndTime === 0 && this._sdkApiCallStartTime !== 0) { - this._sdkApiCallEndTime = performance.now() + this._sdkApiCallEndTime = Date.now() } } @@ -78,7 +78,7 @@ export class TelemetryHelper { public setAllPaginationEndTime() { if (this._allPaginationEndTime === 0 && this._sdkApiCallEndTime !== 0) { - this._allPaginationEndTime = performance.now() + this._allPaginationEndTime = Date.now() } } @@ -88,7 +88,7 @@ export class TelemetryHelper { public setFirstSuggestionShowTime() { if (this._firstSuggestionShowTime === 0 && this._sdkApiCallEndTime !== 0) { - this._firstSuggestionShowTime = performance.now() + this._firstSuggestionShowTime = Date.now() } } diff --git a/packages/amazonq/src/inlineChat/activation.ts b/packages/amazonq/src/inlineChat/activation.ts index 9f196f31ba3..52c826abb61 100644 --- a/packages/amazonq/src/inlineChat/activation.ts +++ b/packages/amazonq/src/inlineChat/activation.ts @@ -5,12 +5,12 @@ import * as vscode from 'vscode' import { InlineChatController } from './controller/inlineChatController' import { registerInlineCommands } from './command/registerInlineCommands' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChatTutorialAnnotation' export function activate( context: vscode.ExtensionContext, - client: LanguageClient, + client: BaseLanguageClient, encryptionKey: Buffer, inlineChatTutorialAnnotation: InlineChatTutorialAnnotation ) { diff --git a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts index 7151a8f9723..472591039f3 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts @@ -14,7 +14,7 @@ import { CodelensProvider } from '../codeLenses/codeLenseProvider' import { PromptMessage, ReferenceLogController } from 'aws-core-vscode/codewhispererChat' import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' import { UserWrittenCodeTracker } from 'aws-core-vscode/codewhisperer' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { codicon, getIcon, @@ -41,7 +41,7 @@ export class InlineChatController { constructor( context: vscode.ExtensionContext, - client: LanguageClient, + client: BaseLanguageClient, encryptionKey: Buffer, inlineChatTutorialAnnotation: InlineChatTutorialAnnotation ) { diff --git a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts index cfa3798945c..c2dc94de36f 100644 --- a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -8,7 +8,7 @@ import { CodeWhispererStreamingServiceException, GenerateAssistantResponseCommandOutput, } from '@amzn/codewhisperer-streaming' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { inlineChatRequestType } from '@aws/language-server-runtimes/protocol' import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' import { @@ -40,7 +40,7 @@ export class InlineChatProvider { public onErrorOccured = this.errorEmitter.event public constructor( - private readonly client: LanguageClient, + private readonly client: BaseLanguageClient, private readonly encryptionKey: Buffer ) { this.editorContextExtractor = new EditorContextExtractor() diff --git a/packages/amazonq/src/lsp/auth.ts b/packages/amazonq/src/lsp/auth.ts index 161ba4d9762..b4093362004 100644 --- a/packages/amazonq/src/lsp/auth.ts +++ b/packages/amazonq/src/lsp/auth.ts @@ -14,12 +14,12 @@ import { } from '@aws/language-server-runtimes/protocol' import * as jose from 'jose' import * as crypto from 'crypto' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { Writable } from 'stream' -import { onceChanged } from 'aws-core-vscode/utils' +import { onceChanged, onceChangedWithComparator } from 'aws-core-vscode/utils' import { getLogger, oneMinute, isSageMaker } from 'aws-core-vscode/shared' -import { isSsoConnection, isIamConnection } from 'aws-core-vscode/auth' +import { isSsoConnection, isIamConnection, areCredentialsEqual } from 'aws-core-vscode/auth' export const encryptionKey = crypto.randomBytes(32) @@ -70,7 +70,7 @@ export const notificationTypes = { export class AmazonQLspAuth { #logErrorIfChanged = onceChanged((s) => getLogger('amazonqLsp').error(s)) constructor( - private readonly client: LanguageClient, + private readonly client: BaseLanguageClient, private readonly authUtil: AuthUtil = AuthUtil.instance ) {} @@ -108,14 +108,17 @@ export class AmazonQLspAuth { this.client.info(`UpdateBearerToken: ${JSON.stringify(request)}`) } - public updateIamCredentials = onceChanged(this._updateIamCredentials.bind(this)) + public updateIamCredentials = onceChangedWithComparator( + this._updateIamCredentials.bind(this), + ([prevCreds], [currentCreds]) => areCredentialsEqual(prevCreds, currentCreds) + ) private async _updateIamCredentials(credentials: any) { getLogger().info( `[SageMaker Debug] Updating IAM credentials - credentials received: ${credentials ? 'YES' : 'NO'}` ) if (credentials) { getLogger().info( - `[SageMaker Debug] IAM credentials structure: accessKeyId=${credentials.accessKeyId ? 'present' : 'missing'}, secretAccessKey=${credentials.secretAccessKey ? 'present' : 'missing'}, sessionToken=${credentials.sessionToken ? 'present' : 'missing'}` + `[SageMaker Debug] IAM credentials structure: accessKeyId=${credentials.accessKeyId ? 'present' : 'missing'}, secretAccessKey=${credentials.secretAccessKey ? 'present' : 'missing'}, sessionToken=${credentials.sessionToken ? 'present' : 'missing'}, expiration=${credentials.expiration ? 'present' : 'missing'}` ) } @@ -160,6 +163,7 @@ export class AmazonQLspAuth { accessKeyId: credentials.accessKeyId, secretAccessKey: credentials.secretAccessKey, sessionToken: credentials.sessionToken, + expiration: credentials.expiration, } const payload = new TextEncoder().encode(JSON.stringify({ data: iamCredentials })) diff --git a/packages/amazonq/src/lsp/chat/activation.ts b/packages/amazonq/src/lsp/chat/activation.ts index 1f443bed875..93f4d68cf2a 100644 --- a/packages/amazonq/src/lsp/chat/activation.ts +++ b/packages/amazonq/src/lsp/chat/activation.ts @@ -4,7 +4,7 @@ */ import { window } from 'vscode' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { AmazonQChatViewProvider } from './webviewProvider' import { focusAmazonQPanel, registerCommands } from './commands' import { @@ -19,7 +19,7 @@ import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhispere import { pushConfigUpdate } from '../config' import { AutoDebugLspClient } from './autoDebug/lsp/autoDebugLspClient' -export async function activate(languageClient: LanguageClient, encryptionKey: Buffer, mynahUIPath: string) { +export async function activate(languageClient: BaseLanguageClient, encryptionKey: Buffer, mynahUIPath: string) { const disposables = globals.context.subscriptions const provider = new AmazonQChatViewProvider(mynahUIPath, languageClient) @@ -83,14 +83,14 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu }) }), Commands.register('aws.amazonq.manageSubscription', () => { - focusAmazonQPanel().catch((e) => languageClient.error(`[VSCode Client] focusAmazonQPanel() failed`)) + focusAmazonQPanel().catch((e: Error) => languageClient.error(`[VSCode Client] focusAmazonQPanel() failed`)) languageClient .sendRequest('workspace/executeCommand', { command: 'aws/chat/manageSubscription', // arguments: [], }) - .catch((e) => { + .catch((e: Error) => { getLogger('amazonqLsp').error('failed request: aws/chat/manageSubscription: %O', e) }) }), diff --git a/packages/amazonq/src/lsp/chat/autoDebug/commands.ts b/packages/amazonq/src/lsp/chat/autoDebug/commands.ts index 54dfd06a1dc..ecdbf80d1e0 100644 --- a/packages/amazonq/src/lsp/chat/autoDebug/commands.ts +++ b/packages/amazonq/src/lsp/chat/autoDebug/commands.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode' import { Commands, getLogger, messages } from 'aws-core-vscode/shared' import { AutoDebugController } from './controller' +import { autoDebugTelemetry } from './telemetry' /** * Auto Debug commands for Amazon Q @@ -72,6 +73,16 @@ export class AutoDebugCommands implements vscode.Disposable { return await action() } catch (error) { this.logger.error(`AutoDebugCommands: Error in ${logContext}: %s`, error) + + // Record telemetry failure based on context + const commandType = + logContext === 'fixWithAmazonQ' + ? 'fixWithQ' + : logContext === 'fixAllWithAmazonQ' + ? 'fixAllWithQ' + : 'explainProblem' + autoDebugTelemetry.recordCommandFailure(commandType, String(error)) + void messages.showMessage('error', 'Amazon Q was not able to fix or explain the problem. Try again shortly') } } @@ -91,13 +102,21 @@ export class AutoDebugCommands implements vscode.Disposable { * Fix with Amazon Q - fixes only the specific issues the user selected */ private async fixWithAmazonQ(range?: vscode.Range, diagnostics?: vscode.Diagnostic[]): Promise { + const problemCount = diagnostics?.length + autoDebugTelemetry.recordCommandInvocation('fixWithQ', problemCount) + await this.executeWithErrorHandling( async () => { const editor = this.checkActiveEditor() if (!editor) { return } + const saved = await editor.document.save() + if (!saved) { + throw new Error('Failed to save document') + } await this.controller.fixSpecificProblems(range, diagnostics) + autoDebugTelemetry.recordCommandSuccess('fixWithQ', problemCount) }, 'Fix with Amazon Q', 'fixWithAmazonQ' @@ -108,13 +127,20 @@ export class AutoDebugCommands implements vscode.Disposable { * Fix All with Amazon Q - processes all errors in the current file */ private async fixAllWithAmazonQ(): Promise { + autoDebugTelemetry.recordCommandInvocation('fixAllWithQ') + await this.executeWithErrorHandling( async () => { const editor = this.checkActiveEditor() if (!editor) { return } - await this.controller.fixAllProblemsInFile(10) // 10 errors per batch + const saved = await editor.document.save() + if (!saved) { + throw new Error('Failed to save document') + } + const problemCount = await this.controller.fixAllProblemsInFile(10) // 10 errors per batch + autoDebugTelemetry.recordCommandSuccess('fixAllWithQ', problemCount) }, 'Fix All with Amazon Q', 'fixAllWithAmazonQ' @@ -125,6 +151,9 @@ export class AutoDebugCommands implements vscode.Disposable { * Explains the problem using Amazon Q */ private async explainProblem(range?: vscode.Range, diagnostics?: vscode.Diagnostic[]): Promise { + const problemCount = diagnostics?.length + autoDebugTelemetry.recordCommandInvocation('explainProblem', problemCount) + await this.executeWithErrorHandling( async () => { const editor = this.checkActiveEditor() @@ -132,6 +161,7 @@ export class AutoDebugCommands implements vscode.Disposable { return } await this.controller.explainProblems(range, diagnostics) + autoDebugTelemetry.recordCommandSuccess('explainProblem', problemCount) }, 'Explain Problem', 'explainProblem' diff --git a/packages/amazonq/src/lsp/chat/autoDebug/controller.ts b/packages/amazonq/src/lsp/chat/autoDebug/controller.ts index 0a0f8e10622..66dcc83b21d 100644 --- a/packages/amazonq/src/lsp/chat/autoDebug/controller.ts +++ b/packages/amazonq/src/lsp/chat/autoDebug/controller.ts @@ -110,32 +110,34 @@ export class AutoDebugController implements vscode.Disposable { /** * Fix with Amazon Q - sends up to 15 error messages one time when user clicks the button */ - public async fixAllProblemsInFile(maxProblems: number = 15): Promise { + public async fixAllProblemsInFile(maxProblems: number = 15): Promise { try { const editor = vscode.window.activeTextEditor if (!editor) { void messages.showMessage('warn', 'No active editor found') - return + return 0 } // Get all diagnostics for the current file const allDiagnostics = vscode.languages.getDiagnostics(editor.document.uri) const errorDiagnostics = this.filterErrorDiagnostics(allDiagnostics) if (errorDiagnostics.length === 0) { - return + return 0 } // Take up to maxProblems errors (15 by default) const diagnosticsToFix = errorDiagnostics.slice(0, maxProblems) const result = await this.getProblemsFromDiagnostics(undefined, diagnosticsToFix) if (!result) { - return + return 0 } const fixMessage = this.createFixMessage(result.editor.document.uri.fsPath, result.problems) await this.sendMessageToChat(fixMessage) + return result.problems.length } catch (error) { this.logger.error('AutoDebugController: Error in fix process: %s', error) + throw error } } diff --git a/packages/amazonq/src/lsp/chat/autoDebug/telemetry.ts b/packages/amazonq/src/lsp/chat/autoDebug/telemetry.ts new file mode 100644 index 00000000000..dec3f424c5a --- /dev/null +++ b/packages/amazonq/src/lsp/chat/autoDebug/telemetry.ts @@ -0,0 +1,71 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { telemetry } from 'aws-core-vscode/telemetry' + +/** + * Auto Debug command types for telemetry tracking + */ +export type AutoDebugCommandType = 'fixWithQ' | 'fixAllWithQ' | 'explainProblem' + +/** + * Telemetry interface for Auto Debug feature + * Tracks usage counts and success rates for the three main commands + */ +export interface AutoDebugTelemetry { + /** + * Record when an auto debug command is invoked + */ + recordCommandInvocation(commandType: AutoDebugCommandType, problemCount?: number): void + + /** + * Record when an auto debug command succeeds + */ + recordCommandSuccess(commandType: AutoDebugCommandType, problemCount?: number): void + + /** + * Record when an auto debug command fails + */ + recordCommandFailure(commandType: AutoDebugCommandType, error?: string, problemCount?: number): void +} + +/** + * Implementation of Auto Debug telemetry tracking + */ +export class AutoDebugTelemetryImpl implements AutoDebugTelemetry { + recordCommandInvocation(commandType: AutoDebugCommandType, problemCount?: number): void { + telemetry.amazonq_autoDebugCommand.emit({ + amazonqAutoDebugCommandType: commandType, + amazonqAutoDebugAction: 'invoked', + amazonqAutoDebugProblemCount: problemCount, + result: 'Succeeded', + }) + } + + recordCommandSuccess(commandType: AutoDebugCommandType, problemCount?: number): void { + telemetry.amazonq_autoDebugCommand.emit({ + amazonqAutoDebugCommandType: commandType, + amazonqAutoDebugAction: 'completed', + amazonqAutoDebugProblemCount: problemCount, + result: 'Succeeded', + }) + } + + recordCommandFailure(commandType: AutoDebugCommandType, error?: string, problemCount?: number): void { + telemetry.amazonq_autoDebugCommand.emit({ + amazonqAutoDebugCommandType: commandType, + amazonqAutoDebugAction: 'completed', + amazonqAutoDebugProblemCount: problemCount, + result: 'Failed', + reason: error ? 'Error' : 'Unknown', + reasonDesc: error?.substring(0, 200), // Truncate to 200 chars as recommended + }) + } +} + +/** + * Global instance of auto debug telemetry + */ +export const autoDebugTelemetry: AutoDebugTelemetry = new AutoDebugTelemetryImpl() diff --git a/packages/amazonq/src/lsp/chat/commands.ts b/packages/amazonq/src/lsp/chat/commands.ts index fca3a132f90..6e4f928f5f1 100644 --- a/packages/amazonq/src/lsp/chat/commands.ts +++ b/packages/amazonq/src/lsp/chat/commands.ts @@ -10,7 +10,6 @@ import { CodeScanIssue, AuthUtil } from 'aws-core-vscode/codewhisperer' import { getLogger } from 'aws-core-vscode/shared' import * as vscode from 'vscode' import * as path from 'path' -import { codeReviewInChat } from '../../app/amazonqScan/models/constants' import { telemetry, AmazonqCodeReviewTool } from 'aws-core-vscode/telemetry' /** @@ -30,7 +29,7 @@ export function registerCommands(provider: AmazonQChatViewProvider) { issue, filePath, 'Explain', - 'Provide a small description of the issue. You must not attempt to fix the issue. You should only give a small summary of it to the user.', + 'Provide a small description of the issue. You must not attempt to fix the issue. You should only give a small summary of it to the user. You must start with the information stored in the recommendation.text field if it is present.', provider, 'explainIssue' ) @@ -68,11 +67,6 @@ export function registerCommands(provider: AmazonQChatViewProvider) { registerShellCommandShortCut('aws.amazonq.rejectCmdExecution', 'reject-shell-command', provider), registerShellCommandShortCut('aws.amazonq.stopCmdExecution', 'stop-shell-command', provider) ) - if (codeReviewInChat) { - globals.context.subscriptions.push( - registerGenericCommand('aws.amazonq.security.scan-statusbar', 'Review', provider) - ) - } } async function handleIssueCommand( diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 16965e2f41f..1541e60b9c5 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -69,7 +69,8 @@ import { } from '@aws/language-server-runtimes/protocol' import { v4 as uuidv4 } from 'uuid' import * as vscode from 'vscode' -import { Disposable, LanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient' +import * as path from 'path' +import { Disposable, BaseLanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient' import { AmazonQChatViewProvider } from './webviewProvider' import { AggregatedCodeScanIssue, @@ -81,22 +82,8 @@ import { SecurityIssueTreeViewProvider, CodeWhispererConstants, } from 'aws-core-vscode/codewhisperer' -import { - amazonQDiffScheme, - AmazonQPromptSettings, - messages, - openUrl, - isTextEditor, - globals, - setContext, -} from 'aws-core-vscode/shared' -import { - DefaultAmazonQAppInitContext, - messageDispatcher, - EditorContentController, - ViewDiffMessage, - referenceLogText, -} from 'aws-core-vscode/amazonq' +import { AmazonQPromptSettings, messages, openUrl, isTextEditor, globals, setContext } from 'aws-core-vscode/shared' +import { DefaultAmazonQAppInitContext, messageDispatcher, referenceLogText } from 'aws-core-vscode/amazonq' import { telemetry } from 'aws-core-vscode/telemetry' import { isValidResponseError } from './error' import { decryptResponse, encryptRequest } from '../encryption' @@ -105,7 +92,7 @@ import { focusAmazonQPanel } from './commands' import { ChatMessage } from '@aws/language-server-runtimes/server-interface' import { CommentUtils } from 'aws-core-vscode/utils' -export function registerActiveEditorChangeListener(languageClient: LanguageClient) { +export function registerActiveEditorChangeListener(languageClient: BaseLanguageClient) { let debounceTimer: NodeJS.Timeout | undefined vscode.window.onDidChangeActiveTextEditor((editor) => { if (debounceTimer) { @@ -120,7 +107,7 @@ export function registerActiveEditorChangeListener(languageClient: LanguageClien } cursorState = getCursorState(editor.selections) } - languageClient.sendNotification(activeEditorChangedNotificationType.method, { + void languageClient.sendNotification(activeEditorChangedNotificationType.method, { textDocument, cursorState, }) @@ -128,7 +115,10 @@ export function registerActiveEditorChangeListener(languageClient: LanguageClien }) } -export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) { +export function registerLanguageServerEventListener( + languageClient: BaseLanguageClient, + provider: AmazonQChatViewProvider +) { languageClient.info( 'Language client received initializeResult from server:', JSON.stringify(languageClient.initializeResult) @@ -142,7 +132,7 @@ export function registerLanguageServerEventListener(languageClient: LanguageClie } // This passes through metric data from LSP events to Toolkit telemetry with all fields from the LSP server - languageClient.onTelemetry((e) => { + languageClient.onTelemetry((e: any) => { const telemetryName: string = e.name languageClient.info(`[VSCode Telemetry] Emitting ${telemetryName} telemetry: ${JSON.stringify(e.data)}`) try { @@ -156,7 +146,7 @@ export function registerLanguageServerEventListener(languageClient: LanguageClie } export function registerMessageListeners( - languageClient: LanguageClient, + languageClient: BaseLanguageClient, provider: AmazonQChatViewProvider, encryptionKey: Buffer ) { @@ -197,7 +187,7 @@ export function registerMessageListeners( languageClient.info( `[VSCode Client] Chat options flags: mcpServers=${pendingChatOptions?.mcpServers}, history=${pendingChatOptions?.history}, export=${pendingChatOptions?.export}, quickActions=[${quickActionsDisplay}]` ) - languageClient.sendNotification(message.command, message.params) + void languageClient.sendNotification(message.command, message.params) } catch (err) { languageClient.error( `[VSCode Client] Failed to send CHAT_OPTIONS after "aws/chat/ready" event: ${(err as Error).message}` @@ -209,7 +199,7 @@ export function registerMessageListeners( languageClient.info('[VSCode Client] Copy to clipboard event received') try { await messages.copyToClipboard(message.params.code) - } catch (e) { + } catch (e: unknown) { languageClient.error(`[VSCode Client] Failed to copy to clipboard: ${(e as Error).message}`) } break @@ -222,7 +212,7 @@ export function registerMessageListeners( textDocument = { uri: editor.document.uri.toString() } } - languageClient.sendNotification(insertToCursorPositionNotificationType.method, { + void languageClient.sendNotification(insertToCursorPositionNotificationType.method, { ...message.params, cursorPosition, textDocument, @@ -290,12 +280,15 @@ export function registerMessageListeners( const cancellationToken = new CancellationTokenSource() chatStreamTokens.set(chatParams.tabId, cancellationToken) - const chatDisposable = languageClient.onProgress(chatRequestType, partialResultToken, (partialResult) => - handlePartialResult(partialResult, encryptionKey, provider, chatParams.tabId).then( - (result) => { - lastPartialResult = result - } - ) + const chatDisposable = languageClient.onProgress( + chatRequestType, + partialResultToken, + (partialResult: any) => + handlePartialResult(partialResult, encryptionKey, provider, chatParams.tabId).then( + (result) => { + lastPartialResult = result + } + ) ) const editor = @@ -409,7 +402,7 @@ export function registerMessageListeners( const quickActionDisposable = languageClient.onProgress( quickActionRequestType, quickActionPartialResultToken, - (partialResult) => + (partialResult: any) => handlePartialResult( partialResult, encryptionKey, @@ -479,7 +472,7 @@ export function registerMessageListeners( break case followUpClickNotificationType.method: if (!isValidAuthFollowUpType(message.params.followUp.type)) { - languageClient.sendNotification(followUpClickNotificationType.method, message.params) + void languageClient.sendNotification(followUpClickNotificationType.method, message.params) } break case buttonClickRequestType.method: { @@ -501,7 +494,7 @@ export function registerMessageListeners( } else if (exitFocus(message.params)) { await setContext('aws.amazonq.amazonqChatLSP.isFocus', false) } - languageClient.sendNotification(message.command, message.params) + void languageClient.sendNotification(message.command, message.params) } break } @@ -611,7 +604,7 @@ export function registerMessageListeners( languageClient.onRequest( ShowDocumentRequest.method, async (params: ShowDocumentParams): Promise> => { - focusAmazonQPanel().catch((e) => languageClient.error(`[VSCode Client] focusAmazonQPanel() failed`)) + focusAmazonQPanel().catch((e: Error) => languageClient.error(`[VSCode Client] focusAmazonQPanel() failed`)) try { const uri = vscode.Uri.parse(params.uri) @@ -664,31 +657,53 @@ export function registerMessageListeners( ) languageClient.onNotification(openFileDiffNotificationType.method, async (params: OpenFileDiffParams) => { - const ecc = new EditorContentController() - const uri = params.originalFileUri - const doc = await vscode.workspace.openTextDocument(uri) - const entireDocumentSelection = new vscode.Selection( - new vscode.Position(0, 0), - new vscode.Position(doc.lineCount - 1, doc.lineAt(doc.lineCount - 1).text.length) - ) - const viewDiffMessage: ViewDiffMessage = { - context: { - activeFileContext: { - filePath: params.originalFileUri, - fileText: params.originalFileContent ?? '', - fileLanguage: undefined, - matchPolicy: undefined, - }, - focusAreaContext: { - selectionInsideExtendedCodeBlock: entireDocumentSelection, - codeBlock: '', - extendedCodeBlock: '', - names: undefined, - }, - }, - code: params.fileContent ?? '', + // Handle both file:// URIs and raw file paths, ensuring proper Windows path handling + let currentFileUri: vscode.Uri + + // Check if it's already a proper file:// URI + if (params.originalFileUri.startsWith('file://')) { + currentFileUri = vscode.Uri.parse(params.originalFileUri) + } else { + // Decode URL-encoded characters and treat as file path + const decodedPath = decodeURIComponent(params.originalFileUri) + currentFileUri = vscode.Uri.file(decodedPath) + } + + const originalContent = params.originalFileContent ?? '' + const fileName = path.basename(currentFileUri.fsPath) + + // Use custom scheme to avoid adding to recent files + const originalFileUri = vscode.Uri.parse(`amazonq-diff:${fileName}_original_${Date.now()}`) + + // Register content provider for the custom scheme + const disposable = vscode.workspace.registerTextDocumentContentProvider('amazonq-diff', { + provideTextDocumentContent: () => originalContent, + }) + + try { + // Open diff view with custom scheme URI (left) vs current file (right) + await vscode.commands.executeCommand( + 'vscode.diff', + originalFileUri, + currentFileUri, + `${vscode.workspace.asRelativePath(currentFileUri)} (Original ↔ Current, Editable)`, + { preview: false } + ) + + // Clean up content provider when diff view is closed + const cleanupDisposable = vscode.window.onDidChangeVisibleTextEditors(() => { + const isDiffViewOpen = vscode.window.visibleTextEditors.some( + (editor) => editor.document.uri.toString() === originalFileUri.toString() + ) + if (!isDiffViewOpen) { + disposable.dispose() + cleanupDisposable.dispose() + } + }) + } catch (error) { + disposable.dispose() + languageClient.error(`[VSCode Client] Failed to open diff view: ${error}`) } - await ecc.viewDiff(viewDiffMessage, amazonQDiffScheme) }) languageClient.onNotification(chatUpdateNotificationType.method, (params: ChatUpdateParams) => { @@ -760,7 +775,7 @@ async function handleCompleteResult( provider: AmazonQChatViewProvider, tabId: string, disposable: Disposable, - languageClient: LanguageClient + languageClient: BaseLanguageClient ) { const decryptedMessage = await decryptResponse(result, encryptionKey) @@ -781,7 +796,7 @@ async function handleCompleteResult( async function handleSecurityFindings( decryptedMessage: { additionalMessages?: ChatMessage[] }, - languageClient: LanguageClient + languageClient: BaseLanguageClient ): Promise { if (decryptedMessage.additionalMessages === undefined || decryptedMessage.additionalMessages.length === 0) { return @@ -830,7 +845,7 @@ async function handleSecurityFindings( async function resolveChatResponse( requestMethod: string, params: any, - languageClient: LanguageClient, + languageClient: BaseLanguageClient, webview: vscode.Webview | undefined ) { const result = await languageClient.sendRequest(requestMethod, params) diff --git a/packages/amazonq/src/lsp/chat/webviewProvider.ts b/packages/amazonq/src/lsp/chat/webviewProvider.ts index 109a6afd10f..b0e0bddc195 100644 --- a/packages/amazonq/src/lsp/chat/webviewProvider.ts +++ b/packages/amazonq/src/lsp/chat/webviewProvider.ts @@ -25,7 +25,7 @@ import { import { AuthUtil, RegionProfile } from 'aws-core-vscode/codewhisperer' import { featureConfig } from 'aws-core-vscode/amazonq' import { getAmazonQLspConfig } from '../config' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' export class AmazonQChatViewProvider implements WebviewViewProvider { public static readonly viewType = 'aws.amazonq.AmazonQChatView' @@ -40,7 +40,7 @@ export class AmazonQChatViewProvider implements WebviewViewProvider { constructor( private readonly mynahUIPath: string, - private readonly languageClient: LanguageClient + private readonly languageClient: BaseLanguageClient ) {} public async resolveWebviewView( diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index bc065c8f620..2255fb80fee 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -5,7 +5,8 @@ import vscode, { version } from 'vscode' import * as nls from 'vscode-nls' -import { LanguageClient, LanguageClientOptions, RequestType, State } from 'vscode-languageclient' +import { BaseLanguageClient, LanguageClientOptions, RequestType, State } from 'vscode-languageclient' +import { LanguageClient } from 'vscode-languageclient/node' import { InlineCompletionManager } from '../app/inline/completion' import { AmazonQLspAuth, encryptionKey, notificationTypes } from './auth' import { @@ -21,6 +22,7 @@ import { import { AuthUtil, CodeWhispererSettings, + FeatureConfigProvider, getSelectedCustomization, TelemetryHelper, vsCodeState, @@ -45,6 +47,7 @@ import { } from 'aws-core-vscode/shared' import { processUtils } from 'aws-core-vscode/shared' import { activate } from './chat/activation' +import { activate as activateInline } from '../app/inline/activation' import { AmazonQResourcePaths } from './lspInstaller' import { ConfigSection, isValidConfigSection, pushConfigUpdate, toAmazonQLSPLogLevel } from './config' import { activate as activateInlineChat } from '../inlineChat/activation' @@ -224,9 +227,7 @@ export async function startLanguageServer( clientOptions ) - const disposable = client.start() - toDispose.push(disposable) - await client.onReady() + await client.start() // Set up connection metadata handler client.onRequest(notificationTypes.getConnectionMetadata.method, () => { @@ -261,14 +262,14 @@ export async function startLanguageServer( return client } -async function initializeAuth(client: LanguageClient): Promise { +async function initializeAuth(client: BaseLanguageClient): Promise { const auth = new AmazonQLspAuth(client) await auth.refreshConnection(true) return auth } // jscpd:ignore-start -async function initializeLanguageServerConfiguration(client: LanguageClient, context: string = 'startup') { +async function initializeLanguageServerConfiguration(client: BaseLanguageClient, context: string = 'startup') { const logger = getLogger('amazonqLsp') if (AuthUtil.instance.isConnectionValid()) { @@ -306,7 +307,7 @@ async function initializeLanguageServerConfiguration(client: LanguageClient, con } } -async function sendProfileToLsp(client: LanguageClient) { +async function sendProfileToLsp(client: BaseLanguageClient) { const logger = getLogger('amazonqLsp') const profileArn = AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn @@ -323,7 +324,7 @@ async function sendProfileToLsp(client: LanguageClient) { async function onLanguageServerReady( extensionContext: vscode.ExtensionContext, auth: AmazonQLspAuth, - client: LanguageClient, + client: BaseLanguageClient, resourcePaths: AmazonQResourcePaths, toDispose: vscode.Disposable[] ) { @@ -338,8 +339,42 @@ async function onLanguageServerReady( // tutorial for inline chat const inlineChatTutorialAnnotation = new InlineChatTutorialAnnotation(inlineTutorialAnnotation) - const inlineManager = new InlineCompletionManager(client, sessionManager, lineTracker, inlineTutorialAnnotation) - inlineManager.registerInlineCompletion() + const enableInlineRollback = FeatureConfigProvider.instance.getPreFlareRollbackGroup() === 'treatment' + if (enableInlineRollback) { + // use VSC inline + getLogger().info('Entering preflare logic') + await activateInline(client) + } else { + // use language server for inline completion + getLogger().info('Entering postflare logic') + const inlineManager = new InlineCompletionManager(client, sessionManager, lineTracker, inlineTutorialAnnotation) + inlineManager.registerInlineCompletion() + toDispose.push( + inlineManager, + Commands.register('aws.amazonq.showPrev', async () => { + await sessionManager.maybeRefreshSessionUx() + await vscode.commands.executeCommand('editor.action.inlineSuggest.showPrevious') + sessionManager.onPrevSuggestion() + }), + Commands.register('aws.amazonq.showNext', async () => { + await sessionManager.maybeRefreshSessionUx() + await vscode.commands.executeCommand('editor.action.inlineSuggest.showNext') + sessionManager.onNextSuggestion() + }), + // this is a workaround since handleDidShowCompletionItem is not public API + Commands.register('aws.amazonq.checkInlineSuggestionVisibility', async () => { + sessionManager.checkInlineSuggestionVisibility() + }), + Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { + vsCodeState.lastManualTriggerTime = performance.now() + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + }), + vscode.workspace.onDidCloseTextDocument(async () => { + await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') + }) + ) + } + activateInlineChat(extensionContext, client, encryptionKey, inlineChatTutorialAnnotation) if (Experiments.instance.get('amazonqChatLSP', true)) { @@ -354,25 +389,6 @@ async function onLanguageServerReady( await initializeLanguageServerConfiguration(client, 'startup') toDispose.push( - inlineManager, - Commands.register('aws.amazonq.showPrev', async () => { - await sessionManager.maybeRefreshSessionUx() - await vscode.commands.executeCommand('editor.action.inlineSuggest.showPrevious') - sessionManager.onPrevSuggestion() - }), - Commands.register('aws.amazonq.showNext', async () => { - await sessionManager.maybeRefreshSessionUx() - await vscode.commands.executeCommand('editor.action.inlineSuggest.showNext') - sessionManager.onNextSuggestion() - }), - // this is a workaround since handleDidShowCompletionItem is not public API - Commands.register('aws.amazonq.checkInlineSuggestionVisibility', async () => { - sessionManager.checkInlineSuggestionVisibility() - }), - Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { - vsCodeState.lastManualTriggerTime = performance.now() - await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') - }), Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { telemetry.record({ traceId: TelemetryHelper.instance.traceId, @@ -398,14 +414,11 @@ async function onLanguageServerReady( getLogger().debug(`codewhisperer: user dismiss tutorial.`) } }), - vscode.workspace.onDidCloseTextDocument(async () => { - await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') - }), AuthUtil.instance.auth.onDidChangeActiveConnection(async () => { await auth.refreshConnection() }), AuthUtil.instance.auth.onDidDeleteConnection(async () => { - client.sendNotification(notificationTypes.deleteBearerToken.method) + void client.sendNotification(notificationTypes.deleteBearerToken.method) }), AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => sendProfileToLsp(client)), vscode.commands.registerCommand('aws.amazonq.getWorkspaceId', async () => { @@ -418,28 +431,28 @@ async function onLanguageServerReady( return workspaceIdResp }), vscode.workspace.onDidCreateFiles((e) => { - client.sendNotification('workspace/didCreateFiles', { + void client.sendNotification('workspace/didCreateFiles', { files: e.files.map((it) => { return { uri: it.fsPath } }), } as CreateFilesParams) }), vscode.workspace.onDidDeleteFiles((e) => { - client.sendNotification('workspace/didDeleteFiles', { + void client.sendNotification('workspace/didDeleteFiles', { files: e.files.map((it) => { return { uri: it.fsPath } }), } as DeleteFilesParams) }), vscode.workspace.onDidRenameFiles((e) => { - client.sendNotification('workspace/didRenameFiles', { + void client.sendNotification('workspace/didRenameFiles', { files: e.files.map((it) => { return { oldUri: it.oldUri.fsPath, newUri: it.newUri.fsPath } }), } as RenameFilesParams) }), vscode.workspace.onDidChangeWorkspaceFolders((e) => { - client.sendNotification('workspace/didChangeWorkspaceFolder', { + void client.sendNotification('workspace/didChangeWorkspaceFolder', { event: { added: e.added.map((it) => { return { @@ -466,7 +479,7 @@ async function onLanguageServerReady( * When the server restarts (likely due to a crash, then the LanguageClient automatically starts it again) * we need to run some server intialization again. */ -function onServerRestartHandler(client: LanguageClient, auth: AmazonQLspAuth) { +function onServerRestartHandler(client: BaseLanguageClient, auth: AmazonQLspAuth) { return client.onDidChangeState(async (e) => { // Ensure we are in a "restart" state if (!(e.oldState === State.Starting && e.newState === State.Running)) { diff --git a/packages/amazonq/src/lsp/config.ts b/packages/amazonq/src/lsp/config.ts index 6b88eb98d21..d33f58d6988 100644 --- a/packages/amazonq/src/lsp/config.ts +++ b/packages/amazonq/src/lsp/config.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode' import { DevSettings, getServiceEnvVarConfig, BaseLspInstaller, getLogger } from 'aws-core-vscode/shared' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { DidChangeConfigurationNotification, updateConfigurationRequestType, @@ -67,7 +67,7 @@ export function toAmazonQLSPLogLevel(logLevel: vscode.LogLevel): LspLogLevel { * different handlers for specific configs. So this determines the correct place to * push the given config. */ -export async function pushConfigUpdate(client: LanguageClient, config: QConfigs) { +export async function pushConfigUpdate(client: BaseLanguageClient, config: QConfigs) { const logger = getLogger('amazonqLsp') switch (config.type) { @@ -81,7 +81,7 @@ export async function pushConfigUpdate(client: LanguageClient, config: QConfigs) break case 'customization': logger.debug(`Pushing customization configuration: ${config.customization || 'undefined'}`) - client.sendNotification(DidChangeConfigurationNotification.type.method, { + void client.sendNotification(DidChangeConfigurationNotification.type.method, { section: 'aws.q', settings: { customization: config.customization }, }) @@ -89,7 +89,7 @@ export async function pushConfigUpdate(client: LanguageClient, config: QConfigs) break case 'logLevel': logger.debug(`Pushing log level configuration`) - client.sendNotification(DidChangeConfigurationNotification.type.method, { + void client.sendNotification(DidChangeConfigurationNotification.type.method, { section: 'aws.logLevel', }) logger.debug(`Log level configuration pushed successfully`) diff --git a/packages/amazonq/test/e2e/inline/inline.test.ts b/packages/amazonq/test/e2e/inline/inline.test.ts index bcc41851eca..43a9f67ab73 100644 --- a/packages/amazonq/test/e2e/inline/inline.test.ts +++ b/packages/amazonq/test/e2e/inline/inline.test.ts @@ -5,10 +5,18 @@ import * as vscode from 'vscode' import assert from 'assert' -import { closeAllEditors, registerAuthHook, TestFolder, toTextEditor, using } from 'aws-core-vscode/test' +import { + closeAllEditors, + getTestWindow, + registerAuthHook, + resetCodeWhispererGlobalVariables, + TestFolder, + toTextEditor, + using, +} from 'aws-core-vscode/test' +import { RecommendationHandler, RecommendationService, session } from 'aws-core-vscode/codewhisperer' import { Commands, globals, sleep, waitUntil, collectionUtil } from 'aws-core-vscode/shared' import { loginToIdC } from '../amazonq/utils/setup' -import { vsCodeState } from 'aws-core-vscode/codewhisperer' describe('Amazon Q Inline', async function () { const retries = 3 @@ -32,6 +40,7 @@ describe('Amazon Q Inline', async function () { const folder = await TestFolder.create() tempFolder = folder.path await closeAllEditors() + await resetCodeWhispererGlobalVariables() }) afterEach(async function () { @@ -45,6 +54,7 @@ describe('Amazon Q Inline', async function () { const events = getUserTriggerDecision() console.table({ 'telemetry events': JSON.stringify(events), + 'recommendation service status': RecommendationService.instance.isRunning, }) } @@ -61,6 +71,31 @@ describe('Amazon Q Inline', async function () { }) } + async function waitForRecommendations() { + const suggestionShown = await waitUntil(async () => session.getSuggestionState(0) === 'Showed', waitOptions) + if (!suggestionShown) { + throw new Error(`Suggestion did not show. Suggestion States: ${JSON.stringify(session.suggestionStates)}`) + } + const suggestionVisible = await waitUntil( + async () => RecommendationHandler.instance.isSuggestionVisible(), + waitOptions + ) + if (!suggestionVisible) { + throw new Error( + `Suggestions failed to become visible. Suggestion States: ${JSON.stringify(session.suggestionStates)}` + ) + } + console.table({ + 'suggestions states': JSON.stringify(session.suggestionStates), + 'valid recommendation': RecommendationHandler.instance.isValidResponse(), + 'recommendation service status': RecommendationService.instance.isRunning, + recommendations: session.recommendations, + }) + if (!RecommendationHandler.instance.isValidResponse()) { + throw new Error('Did not find a valid response') + } + } + /** * Waits for a specific telemetry event to be emitted with the expected suggestion state. * It looks like there might be a potential race condition in codewhisperer causing telemetry @@ -114,9 +149,8 @@ describe('Amazon Q Inline', async function () { await invokeCompletion() originalEditorContents = vscode.window.activeTextEditor?.document.getText() - // wait until all the recommendations have finished - await waitUntil(() => Promise.resolve(vsCodeState.isRecommendationsActive === true), waitOptions) - await waitUntil(() => Promise.resolve(vsCodeState.isRecommendationsActive === false), waitOptions) + // wait until the ghost text appears + await waitForRecommendations() } beforeEach(async () => { @@ -129,12 +163,14 @@ describe('Amazon Q Inline', async function () { try { await setup() console.log(`test run ${attempt} succeeded`) + logUserDecisionStatus() break } catch (e) { console.log(`test run ${attempt} failed`) console.log(e) logUserDecisionStatus() attempt++ + await resetCodeWhispererGlobalVariables() } } if (attempt === retries) { @@ -180,6 +216,29 @@ describe('Amazon Q Inline', async function () { assert.deepStrictEqual(vscode.window.activeTextEditor?.document.getText(), originalEditorContents) }) }) + + it(`${name} invoke on unsupported filetype`, async function () { + await setupEditor({ + name: 'test.zig', + contents: `fn doSomething() void { + + }`, + }) + + /** + * Add delay between editor loading and invoking completion + * @see beforeEach in supported filetypes for more information + */ + await sleep(1000) + await invokeCompletion() + + if (name === 'automatic') { + // It should never get triggered since its not a supported file type + assert.deepStrictEqual(RecommendationService.instance.isRunning, false) + } else { + await getTestWindow().waitForMessage('currently not supported by Amazon Q inline suggestions') + } + }) }) } }) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/EditRendering/stringUtils.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/EditRendering/stringUtils.test.ts new file mode 100644 index 00000000000..09c33fb0c80 --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/apps/inline/EditRendering/stringUtils.test.ts @@ -0,0 +1,47 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'assert' +import { stripCommonIndentation } from '../../../../../../src/app/inline/EditRendering/stringUtils' + +describe('stripCommonIndentation', () => { + it('should strip common leading whitespace', () => { + const input = [' line1 ', ' line2 ', ' line3 '] + const expected = ['line1 ', 'line2 ', ' line3 '] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) + + it('should handle HTML tags', () => { + const input = [ + ' line2 ', + ] + const expected = ['line2 '] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) + + it('should handle mixed indentation', () => { + const input = [' line1', ' line2', ' line3'] + const expected = ['line1', ' line2', ' line3'] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) + + it('should handle empty lines', () => { + const input = [' line1', '', ' line2'] + const expected = [' line1', '', ' line2'] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) + + it('should handle no indentation', () => { + const input = ['line1', 'line2'] + const expected = ['line1', 'line2'] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) + + it('should handle single line', () => { + const input = [' single line'] + const expected = ['single line'] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) +}) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index 417c8be1426..6cf875917ba 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -14,7 +14,7 @@ import { InlineCompletionTriggerKind, } from 'vscode' import assert from 'assert' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { StringValue } from 'vscode-languageserver-types' import { AmazonQInlineCompletionItemProvider, InlineCompletionManager } from '../../../../../src/app/inline/completion' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' @@ -29,10 +29,11 @@ import { import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' import { InlineTutorialAnnotation } from '../../../../../src/app/inline/tutorials/inlineTutorialAnnotation' import { DocumentEventListener } from '../../../../../src/app/inline/documentEventListener' +import { setContext } from 'aws-core-vscode/shared' describe('InlineCompletionManager', () => { let manager: InlineCompletionManager - let languageClient: LanguageClient + let languageClient: BaseLanguageClient let sendNotificationStub: sinon.SinonStub let registerProviderStub: sinon.SinonStub let registerCommandStub: sinon.SinonStub @@ -89,7 +90,7 @@ describe('InlineCompletionManager', () => { languageClient = { sendNotification: sendNotificationStub, - } as unknown as LanguageClient + } as unknown as BaseLanguageClient const sessionManager = new SessionManager() const lineTracker = new LineTracker() @@ -246,7 +247,7 @@ describe('InlineCompletionManager', () => { let inlineTutorialAnnotation: InlineTutorialAnnotation let documentEventListener: DocumentEventListener - beforeEach(() => { + beforeEach(async () => { const lineTracker = new LineTracker() inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, mockSessionManager) recommendationService = new RecommendationService(mockSessionManager) @@ -269,6 +270,9 @@ describe('InlineCompletionManager', () => { getAllRecommendationsStub = sandbox.stub(recommendationService, 'getAllRecommendations') getAllRecommendationsStub.resolves() sandbox.stub(window, 'activeTextEditor').value(createMockTextEditor()) + + // TODO: can we use stub? + await setContext('aws.amazonq.editSuggestionActive', false) }), it('should call recommendation service to get new suggestions(matching typeahead) for new sessions', async () => { provider = new AmazonQInlineCompletionItemProvider( diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts index 7f2bcbb40ea..c8473022118 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -4,7 +4,7 @@ */ import sinon from 'sinon' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { Position, CancellationToken, InlineCompletionItem, InlineCompletionTriggerKind } from 'vscode' import assert from 'assert' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' @@ -21,7 +21,7 @@ const completionApi = 'aws/textDocument/inlineCompletionWithReferences' const editApi = 'aws/textDocument/editCompletion' describe('RecommendationService', () => { - let languageClient: LanguageClient + let languageClient: BaseLanguageClient let sendRequestStub: sinon.SinonStub let sandbox: sinon.SinonSandbox let sessionManager: SessionManager @@ -71,7 +71,7 @@ describe('RecommendationService', () => { languageClient = { sendRequest: sendRequestStub, warn: sandbox.stub(), - } as unknown as LanguageClient + } as unknown as BaseLanguageClient sessionManager = new SessionManager() @@ -335,7 +335,7 @@ describe('RecommendationService', () => { it('should not make completion request when edit suggestion is active', async () => { // Mock EditSuggestionState to return true (edit suggestion is active) - const isEditSuggestionActiveStub = sandbox.stub(EditSuggestionState, 'isEditSuggestionActive').returns(true) + sandbox.stub(EditSuggestionState, 'isEditSuggestionActive').returns(true) const mockResult = { sessionId: 'test-session', @@ -363,16 +363,11 @@ describe('RecommendationService', () => { assert.strictEqual(cs.length, 1) // Only edit call assert.strictEqual(completionCalls.length, 0) // No completion calls assert.strictEqual(editCalls.length, 1) // One edit call - - // Verify the stub was called - sinon.assert.calledOnce(isEditSuggestionActiveStub) }) it('should make completion request when edit suggestion is not active', async () => { // Mock EditSuggestionState to return false (no edit suggestion active) - const isEditSuggestionActiveStub = sandbox - .stub(EditSuggestionState, 'isEditSuggestionActive') - .returns(false) + sandbox.stub(EditSuggestionState, 'isEditSuggestionActive').returns(false) const mockResult = { sessionId: 'test-session', @@ -400,9 +395,6 @@ describe('RecommendationService', () => { assert.strictEqual(cs.length, 2) // Both calls assert.strictEqual(completionCalls.length, 1) // One completion call assert.strictEqual(editCalls.length, 1) // One edit call - - // Verify the stub was called - sinon.assert.calledOnce(isEditSuggestionActiveStub) }) }) }) diff --git a/packages/amazonq/test/unit/amazonq/lsp/auth.test.ts b/packages/amazonq/test/unit/amazonq/lsp/auth.test.ts index d55fef85f39..835a19b65f1 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/auth.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/auth.test.ts @@ -4,7 +4,7 @@ */ import assert from 'assert' import { AmazonQLspAuth } from '../../../../src/lsp/auth' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' describe('AmazonQLspAuth', function () { describe('updateBearerToken', function () { @@ -16,7 +16,7 @@ describe('AmazonQLspAuth', function () { lastSentToken = param }, info: (_message: string, _data: any) => {}, - } as LanguageClient) + } as BaseLanguageClient) await auth.updateBearerToken('firstToken') assert.notDeepStrictEqual(lastSentToken, {}) diff --git a/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts index b2f5958f52b..4c412a17676 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/chat/messages.test.ts @@ -4,7 +4,7 @@ */ import * as sinon from 'sinon' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { registerMessageListeners } from '../../../../../src/lsp/chat/messages' import { AmazonQChatViewProvider } from '../../../../../src/lsp/chat/webviewProvider' @@ -12,7 +12,7 @@ import { secondaryAuth, authConnection, AuthFollowUpType } from 'aws-core-vscode import { messages } from 'aws-core-vscode/shared' describe('registerMessageListeners', () => { - let languageClient: LanguageClient + let languageClient: BaseLanguageClient let provider: AmazonQChatViewProvider let sandbox: sinon.SinonSandbox let messageHandler: (message: any) => void | Promise @@ -28,7 +28,7 @@ describe('registerMessageListeners', () => { sendNotification: sandbox.stub(), onRequest: sandbox.stub(), onNotification: sandbox.stub(), - } as unknown as LanguageClient + } as unknown as BaseLanguageClient provider = { webview: { diff --git a/packages/amazonq/test/unit/amazonq/lsp/client.test.ts b/packages/amazonq/test/unit/amazonq/lsp/client.test.ts index 7c99c47e0ea..8f11c8eaa35 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/client.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/client.test.ts @@ -5,7 +5,7 @@ import assert from 'assert' import sinon from 'sinon' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { AmazonQLspAuth } from '../../../../src/lsp/auth' @@ -108,7 +108,7 @@ describe('Language Server Client Authentication', function () { describe('initializeLanguageServerConfiguration behavior', function () { it('should initialize configuration when connection is valid', async function () { // Test the expected behavior of the function - const mockInitializeFunction = async (client: LanguageClient, context: string) => { + const mockInitializeFunction = async (client: BaseLanguageClient, context: string) => { const { getLogger } = require('aws-core-vscode/shared') const { pushConfigUpdate } = require('../../../../src/lsp/config') const logger = getLogger('amazonqLsp') @@ -178,7 +178,7 @@ describe('Language Server Client Authentication', function () { }, })) - const mockInitializeFunction = async (client: LanguageClient, context: string) => { + const mockInitializeFunction = async (client: BaseLanguageClient, context: string) => { const { getLogger } = require('aws-core-vscode/shared') const logger = getLogger('amazonqLsp') @@ -215,7 +215,7 @@ describe('Language Server Client Authentication', function () { describe('crash recovery handler behavior', function () { it('should reinitialize authentication after crash', async function () { - const mockCrashHandler = async (client: LanguageClient, auth: AmazonQLspAuth) => { + const mockCrashHandler = async (client: BaseLanguageClient, auth: AmazonQLspAuth) => { const { getLogger } = require('aws-core-vscode/shared') const { pushConfigUpdate } = require('../../../../src/lsp/config') const logger = getLogger('amazonqLsp') diff --git a/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts b/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts index 0a8cde5bacf..e02c29dd72e 100644 --- a/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts +++ b/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts @@ -177,7 +177,7 @@ describe('EditDecorationManager', function () { editorStub.setDecorations.reset() // Call clearDecorations - await manager.clearDecorations(editorStub as unknown as vscode.TextEditor) + await manager.clearDecorations(editorStub as unknown as vscode.TextEditor, []) // Verify decorations were cleared assert.strictEqual(editorStub.setDecorations.callCount, 2) @@ -188,7 +188,93 @@ describe('EditDecorationManager', function () { }) }) -describe('displaySvgDecoration cursor distance auto-reject', function () { +describe('displaySvgDecoration cursor distance auto-discard', function () { + let sandbox: sinon.SinonSandbox + let editorStub: sinon.SinonStubbedInstance + let languageClientStub: any + let sessionStub: any + let itemStub: any + + beforeEach(function () { + sandbox = sinon.createSandbox() + const commonStubs = createCommonStubs(sandbox) + editorStub = commonStubs.editorStub + + languageClientStub = { + sendNotification: sandbox.stub(), + } + + sessionStub = { + sessionId: 'test-session', + requestStartTime: Date.now(), + firstCompletionDisplayLatency: 100, + } + + itemStub = { + itemId: 'test-item', + insertText: 'test content', + } + }) + + afterEach(function () { + sandbox.restore() + }) + + it('should send discard telemetry and return early when edit is 10+ lines away from cursor', async function () { + // Set cursor at line 5 + editorStub.selection = { + active: new vscode.Position(5, 0), + } as any + // Try to display edit at line 20 (15 lines away) + await displaySvgDecoration( + editorStub as unknown as vscode.TextEditor, + vscode.Uri.parse('data:image/svg+xml;base64,test'), + 20, + 'new code', + [], + sessionStub, + languageClientStub, + itemStub, + [] + ) + + // Verify discard telemetry was sent + sinon.assert.calledOnce(languageClientStub.sendNotification) + const call = languageClientStub.sendNotification.getCall(0) + assert.strictEqual(call.args[0], 'aws/logInlineCompletionSessionResults') + assert.strictEqual(call.args[1].sessionId, 'test-session') + assert.strictEqual(call.args[1].completionSessionResult['test-item'].discarded, true) + }) + + it('should proceed normally when edit is within 10 lines of cursor', async function () { + // Set cursor at line 5 + editorStub.selection = { + active: new vscode.Position(5, 0), + } as any + // Mock required dependencies for normal flow + sandbox.stub(vscode.workspace, 'onDidChangeTextDocument').returns({ dispose: sandbox.stub() }) + sandbox.stub(vscode.window, 'onDidChangeTextEditorSelection').returns({ dispose: sandbox.stub() }) + + // Try to display edit at line 10 (5 lines away) + await displaySvgDecoration( + editorStub as unknown as vscode.TextEditor, + vscode.Uri.parse('data:image/svg+xml;base64,test'), + 10, + 'new code', + [], + sessionStub, + languageClientStub, + itemStub, + [] + ) + + // Verify no discard telemetry was sent (function should proceed normally) + sinon.assert.notCalled(languageClientStub.sendNotification) + }) +}) + +// TODO: reenable this test, need some updates after refactor +describe.skip('displaySvgDecoration cursor distance auto-reject', function () { let sandbox: sinon.SinonSandbox let editorStub: sinon.SinonStubbedInstance let windowStub: sinon.SinonStub @@ -207,7 +293,8 @@ describe('displaySvgDecoration cursor distance auto-reject', function () { [], {} as any, {} as any, - { itemId: 'test', insertText: 'patch' } as any + { itemId: 'test', insertText: 'patch' } as any, + [] ) } @@ -253,6 +340,10 @@ describe('displaySvgDecoration cursor distance auto-reject', function () { }) it('should not reject when cursor moves less than 25 lines away', async function () { + // Set cursor at line 50 + editorStub.selection = { + active: new vscode.Position(50, 0), + } as any const startLine = 50 await setupDisplaySvgDecoration(startLine) @@ -262,6 +353,10 @@ describe('displaySvgDecoration cursor distance auto-reject', function () { }) it('should not reject when cursor moves exactly 25 lines away', async function () { + // Set cursor at line 50 + editorStub.selection = { + active: new vscode.Position(50, 0), + } as any const startLine = 50 await setupDisplaySvgDecoration(startLine) @@ -271,6 +366,10 @@ describe('displaySvgDecoration cursor distance auto-reject', function () { }) it('should reject when cursor moves more than 25 lines away', async function () { + // Set cursor at line 50 + editorStub.selection = { + active: new vscode.Position(50, 0), + } as any const startLine = 50 await setupDisplaySvgDecoration(startLine) @@ -280,6 +379,10 @@ describe('displaySvgDecoration cursor distance auto-reject', function () { }) it('should reject when cursor moves more than 25 lines before the edit', async function () { + // Set cursor at line 50 + editorStub.selection = { + active: new vscode.Position(50, 0), + } as any const startLine = 50 await setupDisplaySvgDecoration(startLine) @@ -289,6 +392,10 @@ describe('displaySvgDecoration cursor distance auto-reject', function () { }) it('should not reject when edit is near beginning of file and cursor cannot move far enough', async function () { + // Set cursor at line 10 + editorStub.selection = { + active: new vscode.Position(10, 0), + } as any const startLine = 10 await setupDisplaySvgDecoration(startLine) @@ -298,6 +405,10 @@ describe('displaySvgDecoration cursor distance auto-reject', function () { }) it('should not reject when edit suggestion is not active', async function () { + // Set cursor at line 50 + editorStub.selection = { + active: new vscode.Position(50, 0), + } as any editSuggestionStateStub.returns(false) const startLine = 50 diff --git a/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts b/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts index e1c32778d83..dcc40a47ed3 100644 --- a/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts +++ b/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode' import * as sinon from 'sinon' import assert from 'assert' // Remove static import - we'll use dynamic import instead -// import { showEdits } from '../../../../../src/app/inline/EditRendering/imageRenderer' +// import { EditsSuggestionSvg } from '../../../../../src/app/inline/EditRendering/imageRenderer' import { SvgGenerationService } from '../../../../../src/app/inline/EditRendering/svgGenerator' import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol' @@ -19,7 +19,7 @@ describe('showEdits', function () { let displaySvgDecorationStub: sinon.SinonStub let loggerStub: sinon.SinonStubbedInstance let getLoggerStub: sinon.SinonStub - let showEdits: any // Will be dynamically imported + let EditsSuggestionSvgClass: any // Will be dynamically imported let languageClientStub: any let sessionStub: any let itemStub: InlineCompletionItemWithReferences @@ -75,7 +75,7 @@ describe('showEdits', function () { // Now require the module - it should use our mocked getLogger // jscpd:ignore-end const imageRendererModule = require('../../../../../src/app/inline/EditRendering/imageRenderer') - showEdits = imageRendererModule.showEdits + EditsSuggestionSvgClass = imageRendererModule.EditsSuggestionSvg // Create document stub documentStub = { @@ -136,12 +136,12 @@ describe('showEdits', function () { }) it('should return early when editor is undefined', async function () { - await showEdits(itemStub, undefined, sessionStub, languageClientStub) - + const sut = new EditsSuggestionSvgClass(itemStub, undefined as any, languageClientStub, sessionStub) + await sut.show() // Verify that no SVG generation or display methods were called sinon.assert.notCalled(svgGenerationServiceStub.generateDiffSvg) sinon.assert.notCalled(displaySvgDecorationStub) - sinon.assert.notCalled(loggerStub.error) + sinon.assert.calledOnce(loggerStub.error) }) it('should successfully generate and display SVG when all parameters are valid', async function () { @@ -149,8 +149,8 @@ describe('showEdits', function () { const mockSvgResult = createMockSvgResult() svgGenerationServiceStub.generateDiffSvg.resolves(mockSvgResult) - await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) - + const sut = new EditsSuggestionSvgClass(itemStub, editorStub, languageClientStub, sessionStub) + await sut.show() // Verify SVG generation was called with correct parameters sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) sinon.assert.calledWith( @@ -161,17 +161,17 @@ describe('showEdits', function () { // Verify display decoration was called with correct parameters sinon.assert.calledOnce(displaySvgDecorationStub) - sinon.assert.calledWith( - displaySvgDecorationStub, - editorStub, - mockSvgResult.svgImage, - mockSvgResult.startLine, - mockSvgResult.newCode, - mockSvgResult.originalCodeHighlightRange, - sessionStub, - languageClientStub, - itemStub - ) + const ca = displaySvgDecorationStub.getCall(0) + assert.strictEqual(ca.args[0], editorStub) + assert.strictEqual(ca.args[1], mockSvgResult.svgImage) + assert.strictEqual(ca.args[2], mockSvgResult.startLine) + assert.strictEqual(ca.args[3], mockSvgResult.newCode) + assert.strictEqual(ca.args[4], mockSvgResult.originalCodeHighlightRange) + assert.strictEqual(ca.args[5], sessionStub) + assert.strictEqual(ca.args[6], languageClientStub) + assert.strictEqual(ca.args[7], itemStub) + assert.ok(Array.isArray(ca.args[8])) + assert.strictEqual(ca.args[8].length, 2) // Verify no errors were logged sinon.assert.notCalled(loggerStub.error) @@ -182,7 +182,8 @@ describe('showEdits', function () { const mockSvgResult = createMockSvgResult({ svgImage: undefined as any }) svgGenerationServiceStub.generateDiffSvg.resolves(mockSvgResult) - await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + const sut = new EditsSuggestionSvgClass(itemStub, editorStub, languageClientStub, sessionStub) + await sut.show() // Verify SVG generation was called sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) @@ -200,7 +201,8 @@ describe('showEdits', function () { const testError = new Error('SVG generation failed') svgGenerationServiceStub.generateDiffSvg.rejects(testError) - await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + const sut = new EditsSuggestionSvgClass(itemStub, editorStub, languageClientStub, sessionStub) + await sut.show() // Verify SVG generation was called sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) @@ -223,7 +225,8 @@ describe('showEdits', function () { const testError = new Error('Display decoration failed') displaySvgDecorationStub.rejects(testError) - await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + const sut = new EditsSuggestionSvgClass(itemStub, editorStub, languageClientStub, sessionStub) + await sut.show() // Verify SVG generation was called sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) @@ -238,9 +241,11 @@ describe('showEdits', function () { }) it('should use correct logger name', async function () { - await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + const sut = new EditsSuggestionSvgClass(itemStub, editorStub, languageClientStub, sessionStub) + await sut.show() // Verify getLogger was called with correct name + sinon.assert.calledOnce(getLoggerStub) sinon.assert.calledWith(getLoggerStub, 'nextEditPrediction') }) @@ -255,12 +260,8 @@ describe('showEdits', function () { const mockSvgResult = createMockSvgResult() svgGenerationServiceStub.generateDiffSvg.resolves(mockSvgResult) - await showEdits( - itemWithUndefinedText, - editorStub as unknown as vscode.TextEditor, - sessionStub, - languageClientStub - ) + const sut = new EditsSuggestionSvgClass(itemWithUndefinedText, editorStub, languageClientStub, sessionStub) + await sut.show() // Verify SVG generation was called with undefined as string sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) diff --git a/packages/amazonq/test/unit/app/inline/completion.test.ts b/packages/amazonq/test/unit/app/inline/completion.test.ts index bd38b1c95af..5c8673a0276 100644 --- a/packages/amazonq/test/unit/app/inline/completion.test.ts +++ b/packages/amazonq/test/unit/app/inline/completion.test.ts @@ -43,7 +43,7 @@ describe('AmazonQInlineCompletionItemProvider', function () { const session = { sessionId: 'test-session', firstCompletionDisplayLatency: 100, - requestStartTime: performance.now() - 1000, + requestStartTime: Date.now() - 1000, } provider.batchDiscardTelemetryForEditSuggestion(items, session) @@ -84,7 +84,7 @@ describe('AmazonQInlineCompletionItemProvider', function () { const session = { sessionId: 'test-session', firstCompletionDisplayLatency: 100, - requestStartTime: performance.now() - 1000, + requestStartTime: Date.now() - 1000, } provider.batchDiscardTelemetryForEditSuggestion(items, session) @@ -108,7 +108,7 @@ describe('AmazonQInlineCompletionItemProvider', function () { const session = { sessionId: 'test-session', firstCompletionDisplayLatency: 100, - requestStartTime: performance.now() - 1000, + requestStartTime: Date.now() - 1000, } provider.batchDiscardTelemetryForEditSuggestion(items, session) @@ -166,7 +166,7 @@ describe('AmazonQInlineCompletionItemProvider', function () { mockSessionManager.getActiveSession.returns({ displayed: true, suggestions: [{ isInlineEdit: true }], - lastVisibleTime: performance.now(), + lastVisibleTime: Date.now(), }) const result = await provider.isCompletionActive() @@ -176,7 +176,7 @@ describe('AmazonQInlineCompletionItemProvider', function () { }) it('should return true when VS Code command executes successfully', async function () { - const currentTime = performance.now() + const currentTime = Date.now() mockSessionManager.getActiveSession.returns({ displayed: true, suggestions: [{ isInlineEdit: false }], @@ -192,7 +192,7 @@ describe('AmazonQInlineCompletionItemProvider', function () { }) it('should return false when VS Code command fails', async function () { - const oldTime = performance.now() - 100 // Old timestamp (>50ms ago) + const oldTime = Date.now() - 100 // Old timestamp (>50ms ago) mockSessionManager.getActiveSession.returns({ displayed: true, suggestions: [{ isInlineEdit: false }], diff --git a/packages/amazonq/test/unit/app/inline/cursorUpdateManager.test.ts b/packages/amazonq/test/unit/app/inline/cursorUpdateManager.test.ts index 2412e8b3308..aec2c1c7ddf 100644 --- a/packages/amazonq/test/unit/app/inline/cursorUpdateManager.test.ts +++ b/packages/amazonq/test/unit/app/inline/cursorUpdateManager.test.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode' import * as sinon from 'sinon' -import { LanguageClient } from 'vscode-languageclient' +import { BaseLanguageClient } from 'vscode-languageclient' import { CursorUpdateManager } from '../../../../src/app/inline/cursorUpdateManager' import { globals } from 'aws-core-vscode/shared' import assert from 'assert' @@ -14,7 +14,7 @@ import { CodeSuggestionsState } from 'aws-core-vscode/codewhisperer' describe('CursorUpdateManager', () => { let cursorUpdateManager: CursorUpdateManager - let languageClient: LanguageClient + let languageClient: BaseLanguageClient let clock: sinon.SinonFakeTimers let sendRequestStub: sinon.SinonStub let setIntervalStub: sinon.SinonStub @@ -28,7 +28,7 @@ describe('CursorUpdateManager', () => { languageClient = { sendRequest: sendRequestStub, - } as unknown as LanguageClient + } as unknown as BaseLanguageClient // Setup clock stubs clock = sinon.useFakeTimers() diff --git a/packages/amazonq/test/unit/app/inline/notebookUtil.test.ts b/packages/amazonq/test/unit/app/inline/notebookUtil.test.ts new file mode 100644 index 00000000000..697c88ef6ec --- /dev/null +++ b/packages/amazonq/test/unit/app/inline/notebookUtil.test.ts @@ -0,0 +1,87 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as assert from 'assert' +import { createMockDocument } from 'aws-core-vscode/test' +import { convertCellContent, getNotebookContext } from '../../../../src/app/inline/notebookUtil' +import { CodeWhispererConstants } from 'aws-core-vscode/codewhisperer' + +export function createNotebookCell( + document: vscode.TextDocument = createMockDocument('def example():\n return "test"'), + kind: vscode.NotebookCellKind = vscode.NotebookCellKind.Code, + notebook: vscode.NotebookDocument = {} as any, + index: number = 0, + outputs: vscode.NotebookCellOutput[] = [], + metadata: { readonly [key: string]: any } = {}, + executionSummary?: vscode.NotebookCellExecutionSummary +): vscode.NotebookCell { + return { + document, + kind, + notebook, + index, + outputs, + metadata, + executionSummary, + } +} + +describe('Notebook Util', function () { + describe('convertCellContent', function () { + it('should return code cell content as-is', function () { + const codeCell = createNotebookCell( + createMockDocument('def example():\n return "test"'), + vscode.NotebookCellKind.Code + ) + const result = convertCellContent(codeCell) + assert.strictEqual(result, 'def example():\n return "test"') + }) + + it('should convert markdown cell content to comments for Python', function () { + const markdownCell = createNotebookCell( + createMockDocument('# Heading\nSome text'), + vscode.NotebookCellKind.Markup + ) + const result = convertCellContent(markdownCell) + assert.strictEqual(result, '# # Heading\n# Some text') + }) + }) + + describe('getNotebookContext', function () { + it('should combine context from multiple cells', function () { + const currentDoc = createMockDocument('cell2 content', 'b.ipynb') + const notebook = { + getCells: () => [ + createNotebookCell(createMockDocument('cell1 content', 'a.ipynb'), vscode.NotebookCellKind.Code), + createNotebookCell(currentDoc, vscode.NotebookCellKind.Code), + createNotebookCell(createMockDocument('cell3 content', 'c.ipynb'), vscode.NotebookCellKind.Code), + ], + } as vscode.NotebookDocument + + const position = new vscode.Position(0, 5) + + const { caretLeftFileContext, caretRightFileContext } = getNotebookContext(notebook, currentDoc, position) + + assert.strictEqual(caretLeftFileContext, 'cell1 content\ncell2') + assert.strictEqual(caretRightFileContext, ' content\ncell3 content') + }) + + it('should respect character limits', function () { + const longContent = 'a'.repeat(10000) + const notebook = { + getCells: () => [createNotebookCell(createMockDocument(longContent), vscode.NotebookCellKind.Code)], + } as vscode.NotebookDocument + + const currentDoc = createMockDocument(longContent) + const position = new vscode.Position(0, 5000) + + const { caretLeftFileContext, caretRightFileContext } = getNotebookContext(notebook, currentDoc, position) + + assert.ok(caretLeftFileContext.length <= CodeWhispererConstants.charactersLimit) + assert.ok(caretRightFileContext.length <= CodeWhispererConstants.charactersLimit) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts new file mode 100644 index 00000000000..68cebe37bb1 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts @@ -0,0 +1,43 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' +import { + ConfigurationEntry, + invokeRecommendation, + InlineCompletionService, + isInlineCompletionEnabled, + DefaultCodeWhispererClient, +} from 'aws-core-vscode/codewhisperer' + +describe('invokeRecommendation', function () { + describe('invokeRecommendation', function () { + let getRecommendationStub: sinon.SinonStub + let mockClient: DefaultCodeWhispererClient + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + getRecommendationStub = sinon.stub(InlineCompletionService.instance, 'getPaginatedRecommendation') + }) + + afterEach(function () { + sinon.restore() + }) + + it('Should call getPaginatedRecommendation with OnDemand as trigger type when inline completion is enabled', async function () { + const mockEditor = createMockTextEditor() + const config: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + await invokeRecommendation(mockEditor, mockClient, config) + assert.strictEqual(getRecommendationStub.called, isInlineCompletionEnabled()) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts new file mode 100644 index 00000000000..0471aaa3601 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts @@ -0,0 +1,64 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import { onAcceptance, AcceptedSuggestionEntry, session, CodeWhispererTracker } from 'aws-core-vscode/codewhisperer' +import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' + +describe('onAcceptance', function () { + describe('onAcceptance', function () { + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + session.reset() + }) + + afterEach(function () { + sinon.restore() + session.reset() + }) + + it('Should enqueue an event object to tracker', async function () { + const mockEditor = createMockTextEditor() + const trackerSpy = sinon.spy(CodeWhispererTracker.prototype, 'enqueue') + const fakeReferences = [ + { + message: '', + licenseName: 'MIT', + repository: 'http://github.com/fake', + recommendationContentSpan: { + start: 0, + end: 10, + }, + }, + ] + await onAcceptance({ + editor: mockEditor, + range: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 26)), + effectiveRange: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 26)), + acceptIndex: 0, + recommendation: "print('Hello World!')", + requestId: '', + sessionId: '', + triggerType: 'OnDemand', + completionType: 'Line', + language: 'python', + references: fakeReferences, + }) + const actualArg = trackerSpy.getCall(0).args[0] as AcceptedSuggestionEntry + assert.ok(trackerSpy.calledOnce) + assert.strictEqual(actualArg.originalString, 'def two_sum(nums, target):') + assert.strictEqual(actualArg.requestId, '') + assert.strictEqual(actualArg.sessionId, '') + assert.strictEqual(actualArg.triggerType, 'OnDemand') + assert.strictEqual(actualArg.completionType, 'Line') + assert.strictEqual(actualArg.language, 'python') + assert.deepStrictEqual(actualArg.startPosition, new vscode.Position(1, 0)) + assert.deepStrictEqual(actualArg.endPosition, new vscode.Position(1, 26)) + assert.strictEqual(actualArg.index, 0) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts new file mode 100644 index 00000000000..ed3bc99fa34 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts @@ -0,0 +1,43 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' +import { onInlineAcceptance, RecommendationHandler, session } from 'aws-core-vscode/codewhisperer' + +describe('onInlineAcceptance', function () { + describe('onInlineAcceptance', function () { + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + session.reset() + }) + + afterEach(function () { + sinon.restore() + session.reset() + }) + + it('Should dispose inline completion provider', async function () { + const mockEditor = createMockTextEditor() + const spy = sinon.spy(RecommendationHandler.instance, 'disposeInlineCompletion') + await onInlineAcceptance({ + editor: mockEditor, + range: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 21)), + effectiveRange: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 21)), + acceptIndex: 0, + recommendation: "print('Hello World!')", + requestId: '', + sessionId: '', + triggerType: 'OnDemand', + completionType: 'Line', + language: 'python', + references: undefined, + }) + assert.ok(spy.calledWith()) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/codewhisperer.test.ts b/packages/amazonq/test/unit/codewhisperer/service/codewhisperer.test.ts index f30d92de496..9742c69d7df 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/codewhisperer.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/codewhisperer.test.ts @@ -12,7 +12,8 @@ import { codeWhispererClient, } from 'aws-core-vscode/codewhisperer' import { globals, getClientId, getOperatingSystem } from 'aws-core-vscode/shared' -import { AWSError, Request } from 'aws-sdk' +import { Request } from 'aws-sdk' +import { ServiceException } from '@smithy/smithy-client' import { createSpyClient } from 'aws-core-vscode/test' describe('codewhisperer', async function () { @@ -109,7 +110,7 @@ describe('codewhisperer', async function () { requestId: '', }, }), - } as Request) + } as Request) const expectedUserContext = { ideCategory: 'VSCODE', @@ -134,7 +135,7 @@ describe('codewhisperer', async function () { requestId: '', }, }), - } as Request) + } as Request) const authUtilStub = sinon.stub(AuthUtil.instance, 'isValidEnterpriseSsoInUse').returns(isSso) await globals.telemetry.setTelemetryEnabled(isTelemetryEnabled) diff --git a/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts b/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts new file mode 100644 index 00000000000..a35677408c4 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts @@ -0,0 +1,173 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import assert from 'assert' +import * as sinon from 'sinon' +import { + InlineCompletionService, + ReferenceInlineProvider, + RecommendationHandler, + ConfigurationEntry, + CWInlineCompletionItemProvider, + session, + DefaultCodeWhispererClient, +} from 'aws-core-vscode/codewhisperer' +import { createMockTextEditor, resetCodeWhispererGlobalVariables, createMockDocument } from 'aws-core-vscode/test' + +describe('inlineCompletionService', function () { + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + }) + + describe('getPaginatedRecommendation', function () { + const config: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + + let mockClient: DefaultCodeWhispererClient + + beforeEach(async function () { + mockClient = new DefaultCodeWhispererClient() + await resetCodeWhispererGlobalVariables() + }) + + afterEach(function () { + sinon.restore() + }) + + it('should call checkAndResetCancellationTokens before showing inline and next token to be null', async function () { + const mockEditor = createMockTextEditor() + sinon.stub(RecommendationHandler.instance, 'getRecommendations').resolves({ + result: 'Succeeded', + errorMessage: undefined, + recommendationCount: 1, + }) + const checkAndResetCancellationTokensStub = sinon.stub( + RecommendationHandler.instance, + 'checkAndResetCancellationTokens' + ) + session.recommendations = [{ content: "\n\t\tconsole.log('Hello world!');\n\t}" }, { content: '' }] + await InlineCompletionService.instance.getPaginatedRecommendation( + mockClient, + mockEditor, + 'OnDemand', + config + ) + assert.ok(checkAndResetCancellationTokensStub.called) + assert.strictEqual(RecommendationHandler.instance.hasNextToken(), false) + }) + }) + + describe('clearInlineCompletionStates', function () { + it('should remove inline reference and recommendations', async function () { + const fakeReferences = [ + { + message: '', + licenseName: 'MIT', + repository: 'http://github.com/fake', + recommendationContentSpan: { + start: 0, + end: 10, + }, + }, + ] + ReferenceInlineProvider.instance.setInlineReference(1, 'test', fakeReferences) + session.recommendations = [{ content: "\n\t\tconsole.log('Hello world!');\n\t}" }, { content: '' }] + session.language = 'python' + + assert.ok(session.recommendations.length > 0) + await RecommendationHandler.instance.clearInlineCompletionStates() + assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) + assert.strictEqual(session.recommendations.length, 0) + }) + }) + + describe('truncateOverlapWithRightContext', function () { + const fileName = 'test.py' + const language = 'python' + const rightContext = 'return target\n' + const doc = `import math\ndef two_sum(nums, target):\n` + const provider = new CWInlineCompletionItemProvider(0, 0, [], '', new vscode.Position(0, 0), '') + + it('removes overlap with right context from suggestion', async function () { + const mockSuggestion = 'return target\n' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, '') + }) + + it('only removes the overlap part from suggestion', async function () { + const mockSuggestion = 'print(nums)\nreturn target\n' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, 'print(nums)\n') + }) + + it('only removes the last overlap pattern from suggestion', async function () { + const mockSuggestion = 'return target\nprint(nums)\nreturn target\n' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, 'return target\nprint(nums)\n') + }) + + it('returns empty string if the remaining suggestion only contains white space', async function () { + const mockSuggestion = 'return target\n ' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, '') + }) + + it('returns the original suggestion if no match found', async function () { + const mockSuggestion = 'import numpy\n' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, 'import numpy\n') + }) + + it('ignores the space at the end of recommendation', async function () { + const mockSuggestion = 'return target\n\n\n\n\n' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, '') + }) + }) +}) + +describe('CWInlineCompletionProvider', function () { + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + }) + + describe('provideInlineCompletionItems', function () { + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + }) + + afterEach(function () { + sinon.restore() + }) + + it('should return undefined if position is before RecommendationHandler start pos', async function () { + const position = new vscode.Position(0, 0) + const document = createMockDocument() + const fakeContext = { triggerKind: 0, selectedCompletionInfo: undefined } + const token = new vscode.CancellationTokenSource().token + const provider = new CWInlineCompletionItemProvider(0, 0, [], '', new vscode.Position(1, 1), '') + const result = await provider.provideInlineCompletionItems(document, position, fakeContext, token) + + assert.ok(result === undefined) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts new file mode 100644 index 00000000000..4b6a5291f22 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts @@ -0,0 +1,237 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import * as codewhispererSdkClient from 'aws-core-vscode/codewhisperer' +import { + createMockTextEditor, + createTextDocumentChangeEvent, + resetCodeWhispererGlobalVariables, +} from 'aws-core-vscode/test' +import * as EditorContext from 'aws-core-vscode/codewhisperer' +import { + ConfigurationEntry, + DocumentChangedSource, + KeyStrokeHandler, + DefaultDocumentChangedType, + RecommendationService, + ClassifierTrigger, + isInlineCompletionEnabled, + RecommendationHandler, + InlineCompletionService, +} from 'aws-core-vscode/codewhisperer' + +describe('keyStrokeHandler', function () { + const config: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + }) + describe('processKeyStroke', async function () { + let invokeSpy: sinon.SinonStub + let startTimerSpy: sinon.SinonStub + let mockClient: codewhispererSdkClient.DefaultCodeWhispererClient + beforeEach(async function () { + invokeSpy = sinon.stub(KeyStrokeHandler.instance, 'invokeAutomatedTrigger') + startTimerSpy = sinon.stub(KeyStrokeHandler.instance, 'startIdleTimeTriggerTimer') + sinon.spy(RecommendationHandler.instance, 'getRecommendations') + mockClient = new codewhispererSdkClient.DefaultCodeWhispererClient() + await resetCodeWhispererGlobalVariables() + sinon.stub(mockClient, 'listRecommendations') + sinon.stub(mockClient, 'generateRecommendations') + }) + afterEach(function () { + sinon.restore() + }) + + it('Whatever the input is, should skip when automatic trigger is turned off, should not call invokeAutomatedTrigger', async function () { + const mockEditor = createMockTextEditor() + const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( + mockEditor.document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), + ' ' + ) + const cfg: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: false, + isSuggestionsWithCodeReferencesEnabled: true, + } + const keyStrokeHandler = new KeyStrokeHandler() + await keyStrokeHandler.processKeyStroke(mockEvent, mockEditor, mockClient, cfg) + assert.ok(!invokeSpy.called) + assert.ok(!startTimerSpy.called) + }) + + it('Should not call invokeAutomatedTrigger when changed text across multiple lines', async function () { + await testShouldInvoke('\nprint(n', false) + }) + + it('Should not call invokeAutomatedTrigger when doing delete or undo (empty changed text)', async function () { + await testShouldInvoke('', false) + }) + + it('Should call invokeAutomatedTrigger with Enter when inputing \n', async function () { + await testShouldInvoke('\n', true) + }) + + it('Should call invokeAutomatedTrigger with Enter when inputing \r\n', async function () { + await testShouldInvoke('\r\n', true) + }) + + it('Should call invokeAutomatedTrigger with SpecialCharacter when inputing {', async function () { + await testShouldInvoke('{', true) + }) + + it('Should not call invokeAutomatedTrigger for non-special characters for classifier language if classifier says no', async function () { + sinon.stub(ClassifierTrigger.instance, 'shouldTriggerFromClassifier').returns(false) + await testShouldInvoke('a', false) + }) + + it('Should call invokeAutomatedTrigger for non-special characters for classifier language if classifier says yes', async function () { + sinon.stub(ClassifierTrigger.instance, 'shouldTriggerFromClassifier').returns(true) + await testShouldInvoke('a', true) + }) + + it('Should skip invoking if there is immediate right context on the same line and not a single }', async function () { + const casesForSuppressTokenFilling = [ + { + rightContext: 'add', + shouldInvoke: false, + }, + { + rightContext: '}', + shouldInvoke: true, + }, + { + rightContext: '} ', + shouldInvoke: true, + }, + { + rightContext: ')', + shouldInvoke: true, + }, + { + rightContext: ') ', + shouldInvoke: true, + }, + { + rightContext: ' add', + shouldInvoke: true, + }, + { + rightContext: ' ', + shouldInvoke: true, + }, + { + rightContext: '\naddTwo', + shouldInvoke: true, + }, + ] + + for (const o of casesForSuppressTokenFilling) { + await testShouldInvoke('{', o.shouldInvoke, o.rightContext) + } + }) + + async function testShouldInvoke(input: string, shouldTrigger: boolean, rightContext: string = '') { + const mockEditor = createMockTextEditor(rightContext, 'test.js', 'javascript', 0, 0) + const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( + mockEditor.document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), + input + ) + await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, mockClient, config) + assert.strictEqual( + invokeSpy.called, + shouldTrigger, + `invokeAutomatedTrigger ${shouldTrigger ? 'NOT' : 'WAS'} called for rightContext: "${rightContext}"` + ) + } + }) + + describe('invokeAutomatedTrigger', function () { + let mockClient: codewhispererSdkClient.DefaultCodeWhispererClient + beforeEach(async function () { + sinon.restore() + mockClient = new codewhispererSdkClient.DefaultCodeWhispererClient() + await resetCodeWhispererGlobalVariables() + sinon.stub(mockClient, 'listRecommendations') + sinon.stub(mockClient, 'generateRecommendations') + }) + afterEach(function () { + sinon.restore() + }) + + it('should call getPaginatedRecommendation when inline completion is enabled', async function () { + const mockEditor = createMockTextEditor() + const keyStrokeHandler = new KeyStrokeHandler() + const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( + mockEditor.document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), + ' ' + ) + const getRecommendationsStub = sinon.stub(InlineCompletionService.instance, 'getPaginatedRecommendation') + await keyStrokeHandler.invokeAutomatedTrigger('Enter', mockEditor, mockClient, config, mockEvent) + assert.strictEqual(getRecommendationsStub.called, isInlineCompletionEnabled()) + }) + }) + + describe('shouldTriggerIdleTime', function () { + it('should return false when inline is enabled and inline completion is in progress ', function () { + const keyStrokeHandler = new KeyStrokeHandler() + sinon.stub(RecommendationService.instance, 'isRunning').get(() => true) + const result = keyStrokeHandler.shouldTriggerIdleTime() + assert.strictEqual(result, !isInlineCompletionEnabled()) + }) + }) + + describe('test checkChangeSource', function () { + const tabStr = ' '.repeat(EditorContext.getTabSize()) + + const cases: [string, DocumentChangedSource][] = [ + ['\n ', DocumentChangedSource.EnterKey], + ['\n', DocumentChangedSource.EnterKey], + ['(', DocumentChangedSource.SpecialCharsKey], + ['()', DocumentChangedSource.SpecialCharsKey], + ['{}', DocumentChangedSource.SpecialCharsKey], + ['(a, b):', DocumentChangedSource.Unknown], + [':', DocumentChangedSource.SpecialCharsKey], + ['a', DocumentChangedSource.RegularKey], + [tabStr, DocumentChangedSource.TabKey], + [' ', DocumentChangedSource.Reformatting], + ['def add(a,b):\n return a + b\n', DocumentChangedSource.Unknown], + ['function suggestedByIntelliSense():', DocumentChangedSource.Unknown], + ] + + for (const tuple of cases) { + const input = tuple[0] + const expected = tuple[1] + it(`test input ${input} should return ${expected}`, function () { + const actual = new DefaultDocumentChangedType( + createFakeDocumentChangeEvent(tuple[0]) + ).checkChangeSource() + assert.strictEqual(actual, expected) + }) + } + + function createFakeDocumentChangeEvent(str: string): ReadonlyArray { + return [ + { + range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 5)), + rangeOffset: 0, + rangeLength: 0, + text: str, + }, + ] + } + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts new file mode 100644 index 00000000000..08c1b3a7cca --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts @@ -0,0 +1,269 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import { + ReferenceInlineProvider, + session, + AuthUtil, + DefaultCodeWhispererClient, + ConfigurationEntry, + RecommendationHandler, + supplementalContextUtil, +} from 'aws-core-vscode/codewhisperer' +import { + assertTelemetryCurried, + stub, + createMockTextEditor, + resetCodeWhispererGlobalVariables, +} from 'aws-core-vscode/test' +// import * as supplementalContextUtil from 'aws-core-vscode/codewhisperer' + +describe('recommendationHandler', function () { + const config: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + }) + + describe('getRecommendations', async function () { + const mockClient = stub(DefaultCodeWhispererClient) + const mockEditor = createMockTextEditor() + const testStartUrl = 'testStartUrl' + + beforeEach(async function () { + sinon.restore() + await resetCodeWhispererGlobalVariables() + mockClient.listRecommendations.resolves({}) + mockClient.generateRecommendations.resolves({}) + RecommendationHandler.instance.clearRecommendations() + sinon.stub(AuthUtil.instance, 'startUrl').value(testStartUrl) + }) + + afterEach(function () { + sinon.restore() + }) + + // it('should assign correct recommendations given input', async function () { + // assert.strictEqual(CodeWhispererCodeCoverageTracker.instances.size, 0) + // assert.strictEqual( + // CodeWhispererCodeCoverageTracker.getTracker(mockEditor.document.languageId)?.serviceInvocationCount, + // 0 + // ) + + // const mockServerResult = { + // recommendations: [{ content: "print('Hello World!')" }, { content: '' }], + // $response: { + // requestId: 'test_request', + // httpResponse: { + // headers: { + // 'x-amzn-sessionid': 'test_request', + // }, + // }, + // }, + // } + // const handler = new RecommendationHandler() + // sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) + // await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter', false) + // const actual = session.recommendations + // const expected: RecommendationsList = [{ content: "print('Hello World!')" }, { content: '' }] + // assert.deepStrictEqual(actual, expected) + // assert.strictEqual( + // CodeWhispererCodeCoverageTracker.getTracker(mockEditor.document.languageId)?.serviceInvocationCount, + // 1 + // ) + // }) + + it('should assign request id correctly', async function () { + const mockServerResult = { + recommendations: [{ content: "print('Hello World!')" }, { content: '' }], + $response: { + requestId: 'test_request', + httpResponse: { + headers: { + 'x-amzn-sessionid': 'test_request', + }, + }, + }, + } + const handler = new RecommendationHandler() + sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) + sinon.stub(handler, 'isCancellationRequested').returns(false) + await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter', false) + assert.strictEqual(handler.requestId, 'test_request') + assert.strictEqual(session.sessionId, 'test_request') + assert.strictEqual(session.triggerType, 'AutoTrigger') + }) + + it('should call telemetry function that records a CodeWhisperer service invocation', async function () { + const mockServerResult = { + recommendations: [{ content: "print('Hello World!')" }, { content: '' }], + $response: { + requestId: 'test_request', + httpResponse: { + headers: { + 'x-amzn-sessionid': 'test_request', + }, + }, + }, + } + const handler = new RecommendationHandler() + sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) + sinon.stub(supplementalContextUtil, 'fetchSupplementalContext').resolves({ + isUtg: false, + isProcessTimeout: false, + supplementalContextItems: [], + contentsLength: 100, + latency: 0, + strategy: 'empty', + }) + sinon.stub(performance, 'now').returns(0.0) + session.startPos = new vscode.Position(1, 0) + session.startCursorOffset = 2 + await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter') + const assertTelemetry = assertTelemetryCurried('codewhisperer_serviceInvocation') + assertTelemetry({ + codewhispererRequestId: 'test_request', + codewhispererSessionId: 'test_request', + codewhispererLastSuggestionIndex: 1, + codewhispererTriggerType: 'AutoTrigger', + codewhispererAutomatedTriggerType: 'Enter', + codewhispererImportRecommendationEnabled: true, + result: 'Succeeded', + codewhispererLineNumber: 1, + codewhispererCursorOffset: 38, + codewhispererLanguage: 'python', + credentialStartUrl: testStartUrl, + codewhispererSupplementalContextIsUtg: false, + codewhispererSupplementalContextTimeout: false, + codewhispererSupplementalContextLatency: 0, + codewhispererSupplementalContextLength: 100, + }) + }) + }) + + describe('isValidResponse', function () { + afterEach(function () { + sinon.restore() + }) + it('should return true if any response is not empty', function () { + const handler = new RecommendationHandler() + session.recommendations = [ + { + content: + '\n // Use the console to output debug info…n of the command with the "command" variable', + }, + { content: '' }, + ] + assert.ok(handler.isValidResponse()) + }) + + it('should return false if response is empty', function () { + const handler = new RecommendationHandler() + session.recommendations = [] + assert.ok(!handler.isValidResponse()) + }) + + it('should return false if all response has no string length', function () { + const handler = new RecommendationHandler() + session.recommendations = [{ content: '' }, { content: '' }] + assert.ok(!handler.isValidResponse()) + }) + }) + + describe('setCompletionType/getCompletionType', function () { + beforeEach(function () { + sinon.restore() + }) + + it('should set the completion type to block given a multi-line suggestion', function () { + session.setCompletionType(0, { content: 'test\n\n \t\r\nanother test' }) + assert.strictEqual(session.getCompletionType(0), 'Block') + + session.setCompletionType(0, { content: 'test\ntest\n' }) + assert.strictEqual(session.getCompletionType(0), 'Block') + + session.setCompletionType(0, { content: '\n \t\r\ntest\ntest' }) + assert.strictEqual(session.getCompletionType(0), 'Block') + }) + + it('should set the completion type to line given a single-line suggestion', function () { + session.setCompletionType(0, { content: 'test' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + + session.setCompletionType(0, { content: 'test\r\t ' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + }) + + it('should set the completion type to line given a multi-line completion but only one-lien of non-blank sequence', function () { + session.setCompletionType(0, { content: 'test\n\t' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + + session.setCompletionType(0, { content: 'test\n ' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + + session.setCompletionType(0, { content: 'test\n\r' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + + session.setCompletionType(0, { content: '\n\n\n\ntest' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + }) + }) + + describe('on event change', async function () { + beforeEach(function () { + const fakeReferences = [ + { + message: '', + licenseName: 'MIT', + repository: 'http://github.com/fake', + recommendationContentSpan: { + start: 0, + end: 10, + }, + }, + ] + ReferenceInlineProvider.instance.setInlineReference(1, 'test', fakeReferences) + session.sessionId = '' + RecommendationHandler.instance.requestId = '' + }) + + it('should remove inline reference onEditorChange', async function () { + session.sessionId = 'aSessionId' + RecommendationHandler.instance.requestId = 'aRequestId' + await RecommendationHandler.instance.onEditorChange() + assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) + }) + it('should remove inline reference onFocusChange', async function () { + session.sessionId = 'aSessionId' + RecommendationHandler.instance.requestId = 'aRequestId' + await RecommendationHandler.instance.onFocusChange() + assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) + }) + it('should not remove inline reference on cursor change from typing', async function () { + await RecommendationHandler.instance.onCursorChange({ + textEditor: createMockTextEditor(), + selections: [], + kind: vscode.TextEditorSelectionChangeKind.Keyboard, + }) + assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 1) + }) + + it('should remove inline reference on cursor change from mouse movement', async function () { + await RecommendationHandler.instance.onCursorChange({ + textEditor: vscode.window.activeTextEditor!, + selections: [], + kind: vscode.TextEditorSelectionChangeKind.Mouse, + }) + assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts index 6a74be85118..d72e1f8636f 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts @@ -150,7 +150,7 @@ describe('SecurityIssueTreeViewProvider', function () { item.iconPath?.toString().includes(`${item.issue.severity.toLowerCase()}.svg`) ) ) - assert.ok(issueItems.every((item) => !item.description?.toString().startsWith('[Ln '))) + assert.ok(issueItems.every((item) => item.description?.toString().startsWith('[Ln '))) } }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts new file mode 100644 index 00000000000..ee001b3328d --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts @@ -0,0 +1,560 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { + CodeWhispererCodeCoverageTracker, + vsCodeState, + TelemetryHelper, + AuthUtil, + getUnmodifiedAcceptedTokens, +} from 'aws-core-vscode/codewhisperer' +import { createMockDocument, createMockTextEditor, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' +import { globals } from 'aws-core-vscode/shared' +import { assertTelemetryCurried } from 'aws-core-vscode/test' + +describe('codewhispererCodecoverageTracker', function () { + const language = 'python' + + describe('test getTracker', function () { + afterEach(async function () { + await resetCodeWhispererGlobalVariables() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('unsupported language', function () { + assert.strictEqual(CodeWhispererCodeCoverageTracker.getTracker('vb'), undefined) + assert.strictEqual(CodeWhispererCodeCoverageTracker.getTracker('ipynb'), undefined) + }) + + it('supported language', function () { + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('python'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('javascriptreact'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('java'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('javascript'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('cpp'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('ruby'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('go'), undefined) + }) + + it('supported language and should return singleton object per language', function () { + let instance1: CodeWhispererCodeCoverageTracker | undefined + let instance2: CodeWhispererCodeCoverageTracker | undefined + instance1 = CodeWhispererCodeCoverageTracker.getTracker('java') + instance2 = CodeWhispererCodeCoverageTracker.getTracker('java') + assert.notStrictEqual(instance1, undefined) + assert.strictEqual(Object.is(instance1, instance2), true) + + instance1 = CodeWhispererCodeCoverageTracker.getTracker('python') + instance2 = CodeWhispererCodeCoverageTracker.getTracker('python') + assert.notStrictEqual(instance1, undefined) + assert.strictEqual(Object.is(instance1, instance2), true) + + instance1 = CodeWhispererCodeCoverageTracker.getTracker('javascriptreact') + instance2 = CodeWhispererCodeCoverageTracker.getTracker('javascriptreact') + assert.notStrictEqual(instance1, undefined) + assert.strictEqual(Object.is(instance1, instance2), true) + }) + }) + + describe('test isActive', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + afterEach(async function () { + await resetCodeWhispererGlobalVariables() + CodeWhispererCodeCoverageTracker.instances.clear() + sinon.restore() + }) + + it('inactive case: telemetryEnable = true, isConnected = false', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) + sinon.stub(AuthUtil.instance, 'isConnected').returns(false) + + tracker = CodeWhispererCodeCoverageTracker.getTracker('python') + if (!tracker) { + assert.fail() + } + + assert.strictEqual(tracker.isActive(), false) + }) + + it('inactive case: telemetryEnabled = false, isConnected = false', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(false) + sinon.stub(AuthUtil.instance, 'isConnected').returns(false) + + tracker = CodeWhispererCodeCoverageTracker.getTracker('java') + if (!tracker) { + assert.fail() + } + + assert.strictEqual(tracker.isActive(), false) + }) + + it('active case: telemetryEnabled = true, isConnected = true', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) + sinon.stub(AuthUtil.instance, 'isConnected').returns(true) + + tracker = CodeWhispererCodeCoverageTracker.getTracker('javascript') + if (!tracker) { + assert.fail() + } + assert.strictEqual(tracker.isActive(), true) + }) + }) + + describe('updateAcceptedTokensCount', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('Should compute edit distance to update the accepted tokens', function () { + if (!tracker) { + assert.fail() + } + const editor = createMockTextEditor('def addTwoNumbers(a, b):\n') + + tracker.addAcceptedTokens(editor.document.fileName, { + range: new vscode.Range(0, 0, 0, 25), + text: `def addTwoNumbers(x, y):\n`, + accepted: 25, + }) + tracker.addTotalTokens(editor.document.fileName, 100) + tracker.updateAcceptedTokensCount(editor) + assert.strictEqual(tracker?.acceptedTokens[editor.document.fileName][0].accepted, 23) + }) + }) + + describe('getUnmodifiedAcceptedTokens', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('Should return correct unmodified accepted tokens count', function () { + assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'fou'), 2) + assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'f11111oo'), 3) + assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'fo'), 2) + assert.strictEqual(getUnmodifiedAcceptedTokens('helloworld', 'HelloWorld'), 8) + assert.strictEqual(getUnmodifiedAcceptedTokens('helloworld', 'World'), 4) + assert.strictEqual(getUnmodifiedAcceptedTokens('CodeWhisperer', 'CODE'), 1) + assert.strictEqual(getUnmodifiedAcceptedTokens('CodeWhisperer', 'CodeWhispererGood'), 13) + }) + }) + + describe('countAcceptedTokens', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('Should skip when tracker is not active', function () { + if (!tracker) { + assert.fail() + } + tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') + const spy = sinon.spy(CodeWhispererCodeCoverageTracker.prototype, 'addAcceptedTokens') + assert.ok(!spy.called) + }) + + it('Should increase AcceptedTokens', function () { + if (!tracker) { + assert.fail() + } + tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') + assert.deepStrictEqual(tracker.acceptedTokens['test.py'][0], { + range: new vscode.Range(0, 0, 0, 1), + text: 'a', + accepted: 1, + }) + }) + it('Should increase TotalTokens', function () { + if (!tracker) { + assert.fail() + } + tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') + tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'b', 'test.py') + assert.deepStrictEqual(tracker.totalTokens['test.py'], 2) + }) + }) + + describe('countTotalTokens', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('Should skip when content change size is more than 50', function () { + if (!tracker) { + assert.fail() + } + tracker.countTotalTokens({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 600), + rangeOffset: 0, + rangeLength: 600, + text: 'def twoSum(nums, target):\nfor '.repeat(20), + }, + ], + }) + assert.strictEqual(Object.keys(tracker.totalTokens).length, 0) + }) + + it('Should not skip when content change size is less than 50', function () { + if (!tracker) { + assert.fail() + } + tracker.countTotalTokens({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 49), + rangeOffset: 0, + rangeLength: 49, + text: 'a = 123'.repeat(7), + }, + ], + }) + assert.strictEqual(Object.keys(tracker.totalTokens).length, 1) + assert.strictEqual(Object.values(tracker.totalTokens)[0], 49) + }) + + it('Should skip when CodeWhisperer is editing', function () { + if (!tracker) { + assert.fail() + } + vsCodeState.isCodeWhispererEditing = true + tracker.countTotalTokens({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 30), + rangeOffset: 0, + rangeLength: 30, + text: 'def twoSum(nums, target):\nfor', + }, + ], + }) + const startedSpy = sinon.spy(CodeWhispererCodeCoverageTracker.prototype, 'addTotalTokens') + assert.ok(!startedSpy.called) + }) + + it('Should not reduce tokens when delete', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('import math', 'test.py', 'python') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 0, + rangeLength: 0, + text: 'a', + }, + ], + }) + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 0, + rangeLength: 0, + text: 'b', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 1, + rangeLength: 1, + text: '', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) + }) + + it('Should add tokens when type', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('import math', 'test.py', 'python') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 0, + rangeLength: 0, + text: 'a', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) + }) + + it('Should add tokens when hitting enter with indentation', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('def h():', 'test.py', 'python') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 8), + rangeOffset: 0, + rangeLength: 0, + text: '\n ', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) + }) + + it('Should add tokens when hitting enter with indentation in Windows', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('def h():', 'test.py', 'python') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 8), + rangeOffset: 0, + rangeLength: 0, + text: '\r\n ', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) + }) + + it('Should add tokens when hitting enter with indentation in Java', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('class A() {', 'test.java', 'java') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 11), + rangeOffset: 0, + rangeLength: 0, + text: '', + }, + { + range: new vscode.Range(0, 0, 0, 11), + rangeOffset: 0, + rangeLength: 0, + text: '\n\t\t', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) + }) + + it('Should add tokens when inserting closing brackets', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('a=', 'test.py', 'python') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 3), + rangeOffset: 0, + rangeLength: 0, + text: '[]', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) + }) + + it('Should add tokens when inserting closing brackets in Java', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('class A ', 'test.java', 'java') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 8), + rangeOffset: 0, + rangeLength: 0, + text: '{}', + }, + { + range: new vscode.Range(0, 0, 0, 8), + rangeOffset: 0, + rangeLength: 0, + text: '', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) + }) + }) + + describe('flush', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('Should not send codecoverage telemetry if tracker is not active', function () { + if (!tracker) { + assert.fail() + } + sinon.restore() + sinon.stub(tracker, 'isActive').returns(false) + + tracker.addAcceptedTokens(`test.py`, { range: new vscode.Range(0, 0, 0, 7), text: `print()`, accepted: 7 }) + tracker.addTotalTokens(`test.py`, 100) + tracker.flush() + const data = globals.telemetry.logger.query({ + metricName: 'codewhisperer_codePercentage', + excludeKeys: ['awsAccount'], + }) + assert.strictEqual(data.length, 0) + }) + }) + + describe('emitCodeWhispererCodeContribution', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('should emit correct code coverage telemetry in python file', async function () { + const tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + + const assertTelemetry = assertTelemetryCurried('codewhisperer_codePercentage') + tracker?.incrementServiceInvocationCount() + tracker?.addAcceptedTokens(`test.py`, { range: new vscode.Range(0, 0, 0, 7), text: `print()`, accepted: 7 }) + tracker?.addTotalTokens(`test.py`, 100) + tracker?.emitCodeWhispererCodeContribution() + assertTelemetry({ + codewhispererTotalTokens: 100, + codewhispererLanguage: language, + codewhispererAcceptedTokens: 7, + codewhispererSuggestedTokens: 7, + codewhispererPercentage: 7, + successCount: 1, + }) + }) + + it('should emit correct code coverage telemetry when success count = 0', async function () { + const tracker = CodeWhispererCodeCoverageTracker.getTracker('java') + + const assertTelemetry = assertTelemetryCurried('codewhisperer_codePercentage') + tracker?.addAcceptedTokens(`test.java`, { + range: new vscode.Range(0, 0, 0, 18), + text: `public static main`, + accepted: 18, + }) + tracker?.incrementServiceInvocationCount() + tracker?.incrementServiceInvocationCount() + tracker?.addTotalTokens(`test.java`, 30) + tracker?.emitCodeWhispererCodeContribution() + assertTelemetry({ + codewhispererTotalTokens: 30, + codewhispererLanguage: 'java', + codewhispererAcceptedTokens: 18, + codewhispererSuggestedTokens: 18, + codewhispererPercentage: 60, + successCount: 2, + }) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts b/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts new file mode 100644 index 00000000000..0a3c4b17d60 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts @@ -0,0 +1,117 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { BM25Okapi } from 'aws-core-vscode/codewhisperer' + +describe('bm25', function () { + it('simple case 1', function () { + const query = 'windy London' + const corpus = ['Hello there good man!', 'It is quite windy in London', 'How is the weather today?'] + + const sut = new BM25Okapi(corpus) + const actual = sut.score(query) + + assert.deepStrictEqual(actual, [ + { + content: 'Hello there good man!', + index: 0, + score: 0, + }, + { + content: 'It is quite windy in London', + index: 1, + score: 0.937294722506405, + }, + { + content: 'How is the weather today?', + index: 2, + score: 0, + }, + ]) + + assert.deepStrictEqual(sut.topN(query, 1), [ + { + content: 'It is quite windy in London', + index: 1, + score: 0.937294722506405, + }, + ]) + }) + + it('simple case 2', function () { + const query = 'codewhisperer is a machine learning powered code generator' + const corpus = [ + 'codewhisperer goes GA at April 2023', + 'machine learning tool is the trending topic!!! :)', + 'codewhisperer is good =))))', + 'codewhisperer vs. copilot, which code generator better?', + 'copilot is a AI code generator too', + 'it is so amazing!!', + ] + + const sut = new BM25Okapi(corpus) + const actual = sut.score(query) + + assert.deepStrictEqual(actual, [ + { + content: 'codewhisperer goes GA at April 2023', + index: 0, + score: 0, + }, + { + content: 'machine learning tool is the trending topic!!! :)', + index: 1, + score: 2.597224531416621, + }, + { + content: 'codewhisperer is good =))))', + index: 2, + score: 0.3471790843435529, + }, + { + content: 'codewhisperer vs. copilot, which code generator better?', + index: 3, + score: 1.063018436525109, + }, + { + content: 'copilot is a AI code generator too', + index: 4, + score: 2.485359418462239, + }, + { + content: 'it is so amazing!!', + index: 5, + score: 0.3154033715392277, + }, + ]) + + assert.deepStrictEqual(sut.topN(query, 1), [ + { + content: 'machine learning tool is the trending topic!!! :)', + index: 1, + score: 2.597224531416621, + }, + ]) + + assert.deepStrictEqual(sut.topN(query, 3), [ + { + content: 'machine learning tool is the trending topic!!! :)', + index: 1, + score: 2.597224531416621, + }, + { + content: 'copilot is a AI code generator too', + index: 4, + score: 2.485359418462239, + }, + { + content: 'codewhisperer vs. copilot, which code generator better?', + index: 3, + score: 1.063018436525109, + }, + ]) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts new file mode 100644 index 00000000000..2a2ad8bb34e --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts @@ -0,0 +1,327 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + PlatformLanguageId, + extractClasses, + extractFunctions, + isTestFile, + utgLanguageConfigs, +} from 'aws-core-vscode/codewhisperer' +import assert from 'assert' +import { createTestWorkspaceFolder, toTextDocument } from 'aws-core-vscode/test' + +describe('RegexValidationForPython', () => { + it('should extract all function names from a python file content', () => { + // TODO: Replace this variable based testing to read content from File. + // const filePath = vscode.Uri.file('./testData/samplePython.py').fsPath; + // const fileContent = fs.readFileSync('./testData/samplePython.py' , 'utf-8'); + // const regex = /function\s+(\w+)/g; + + const result = extractFunctions(pythonFileContent, utgLanguageConfigs['python'].functionExtractionPattern) + assert.strictEqual(result.length, 13) + assert.deepStrictEqual(result, [ + 'hello_world', + 'add_numbers', + 'multiply_numbers', + 'sum_numbers', + 'divide_numbers', + '__init__', + 'add', + 'multiply', + 'square', + 'from_sum', + '__init__', + 'triple', + 'main', + ]) + }) + + it('should extract all class names from a file content', () => { + const result = extractClasses(pythonFileContent, utgLanguageConfigs['python'].classExtractionPattern) + assert.deepStrictEqual(result, ['Calculator']) + }) +}) + +describe('RegexValidationForJava', () => { + it('should extract all function names from a java file content', () => { + // TODO: Replace this variable based testing to read content from File. + // const filePath = vscode.Uri.file('./testData/samplePython.py').fsPath; + // const fileContent = fs.readFileSync('./testData/samplePython.py' , 'utf-8'); + // const regex = /function\s+(\w+)/g; + + const result = extractFunctions(javaFileContent, utgLanguageConfigs['java'].functionExtractionPattern) + assert.strictEqual(result.length, 5) + assert.deepStrictEqual(result, ['sayHello', 'doSomething', 'square', 'manager', 'ABCFUNCTION']) + }) + + it('should extract all class names from a java file content', () => { + const result = extractClasses(javaFileContent, utgLanguageConfigs['java'].classExtractionPattern) + assert.deepStrictEqual(result, ['Test']) + }) +}) + +describe('isTestFile', () => { + let testWsFolder: string + beforeEach(async function () { + testWsFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + it('validate by file path', async function () { + const langs = new Map([ + ['java', '.java'], + ['python', '.py'], + ['typescript', '.ts'], + ['javascript', '.js'], + ['typescriptreact', '.tsx'], + ['javascriptreact', '.jsx'], + ]) + const testFilePathsWithoutExt = [ + '/test/MyClass', + '/test/my_class', + '/tst/MyClass', + '/tst/my_class', + '/tests/MyClass', + '/tests/my_class', + ] + + const srcFilePathsWithoutExt = [ + '/src/MyClass', + 'MyClass', + 'foo/bar/MyClass', + 'foo/my_class', + 'my_class', + 'anyFolderOtherThanTest/foo/myClass', + ] + + for (const [languageId, ext] of langs) { + const testFilePaths = testFilePathsWithoutExt.map((it) => it + ext) + for (const testFilePath of testFilePaths) { + const actual = await isTestFile(testFilePath, { languageId: languageId }) + assert.strictEqual(actual, true) + } + + const srcFilePaths = srcFilePathsWithoutExt.map((it) => it + ext) + for (const srcFilePath of srcFilePaths) { + const actual = await isTestFile(srcFilePath, { languageId: languageId }) + assert.strictEqual(actual, false) + } + } + }) + + async function assertIsTestFile( + fileNames: string[], + config: { languageId: PlatformLanguageId }, + expected: boolean + ) { + for (const fileName of fileNames) { + const document = await toTextDocument('', fileName, testWsFolder) + const actual = await isTestFile(document.uri.fsPath, { languageId: config.languageId }) + assert.strictEqual(actual, expected) + } + } + + it('validate by file name', async function () { + const camelCaseSrc = ['Foo.java', 'Bar.java', 'Baz.java'] + await assertIsTestFile(camelCaseSrc, { languageId: 'java' }, false) + + const camelCaseTst = ['FooTest.java', 'BarTests.java'] + await assertIsTestFile(camelCaseTst, { languageId: 'java' }, true) + + const snakeCaseSrc = ['foo.py', 'bar.py'] + await assertIsTestFile(snakeCaseSrc, { languageId: 'python' }, false) + + const snakeCaseTst = ['test_foo.py', 'bar_test.py'] + await assertIsTestFile(snakeCaseTst, { languageId: 'python' }, true) + + const javascriptSrc = ['Foo.js', 'bar.js'] + await assertIsTestFile(javascriptSrc, { languageId: 'javascript' }, false) + + const javascriptTst = ['Foo.test.js', 'Bar.spec.js'] + await assertIsTestFile(javascriptTst, { languageId: 'javascript' }, true) + + const typescriptSrc = ['Foo.ts', 'bar.ts'] + await assertIsTestFile(typescriptSrc, { languageId: 'typescript' }, false) + + const typescriptTst = ['Foo.test.ts', 'Bar.spec.ts'] + await assertIsTestFile(typescriptTst, { languageId: 'typescript' }, true) + + const jsxSrc = ['Foo.jsx', 'Bar.jsx'] + await assertIsTestFile(jsxSrc, { languageId: 'javascriptreact' }, false) + + const jsxTst = ['Foo.test.jsx', 'Bar.spec.jsx'] + await assertIsTestFile(jsxTst, { languageId: 'javascriptreact' }, true) + }) + + it('should return true if the file name matches the test filename pattern - Java', async () => { + const filePaths = ['/path/to/MyClassTest.java', '/path/to/TestMyClass.java', '/path/to/MyClassTests.java'] + const language = 'java' + + for (const filePath of filePaths) { + const result = await isTestFile(filePath, { languageId: language }) + assert.strictEqual(result, true) + } + }) + + it('should return false if the file name does not match the test filename pattern - Java', async () => { + const filePaths = ['/path/to/MyClass.java', '/path/to/MyClass_test.java', '/path/to/test_MyClass.java'] + const language = 'java' + + for (const filePath of filePaths) { + const result = await isTestFile(filePath, { languageId: language }) + assert.strictEqual(result, false) + } + }) + + it('should return true if the file name does not match the test filename pattern - Python', async () => { + const filePaths = ['/path/to/util_test.py', '/path/to/test_util.py'] + const language = 'python' + + for (const filePath of filePaths) { + const result = await isTestFile(filePath, { languageId: language }) + assert.strictEqual(result, true) + } + }) + + it('should return false if the file name does not match the test filename pattern - Python', async () => { + const filePaths = ['/path/to/util.py', '/path/to/utilTest.java', '/path/to/Testutil.java'] + const language = 'python' + + for (const filePath of filePaths) { + const result = await isTestFile(filePath, { languageId: language }) + assert.strictEqual(result, false) + } + }) + + it('should return false if the language is not supported', async () => { + const filePath = '/path/to/MyClass.cpp' + const language = 'c++' + const result = await isTestFile(filePath, { languageId: language }) + assert.strictEqual(result, false) + }) +}) + +const pythonFileContent = ` +# Single-line import statements +import os +import numpy as np +from typing import List, Tuple + +# Multi-line import statements +from collections import ( + defaultdict, + Counter +) + +# Relative imports +from . import module1 +from ..subpackage import module2 + +# Wildcard imports +from mypackage import * +from mypackage.module import * + +# Aliased imports +import pandas as pd +from mypackage import module1 as m1, module2 as m2 + +def hello_world(): + print("Hello, world!") + +def add_numbers(x, y): + return x + y + +def multiply_numbers(x=1, y=1): + return x * y + +def sum_numbers(*args): + total = 0 + for num in args: + total += num + return total + +def divide_numbers(x, y=1, *args, **kwargs): + result = x / y + for arg in args: + result /= arg + for _, value in kwargs.items(): + result /= value + return result + +class Calculator: + def __init__(self, x, y): + self.x = x + self.y = y + + def add(self): + return self.x + self.y + + def multiply(self): + return self.x * self.y + + @staticmethod + def square(x): + return x ** 2 + + @classmethod + def from_sum(cls, x, y): + return cls(x+y, 0) + + class InnerClass: + def __init__(self, z): + self.z = z + + def triple(self): + return self.z * 3 + +def main(): + print(hello_world()) + print(add_numbers(3, 5)) + print(multiply_numbers(3, 5)) + print(sum_numbers(1, 2, 3, 4, 5)) + print(divide_numbers(10, 2, 5, 2, a=2, b=3)) + + calc = Calculator(3, 5) + print(calc.add()) + print(calc.multiply()) + print(Calculator.square(3)) + print(Calculator.from_sum(2, 3).add()) + + inner = Calculator.InnerClass(5) + print(inner.triple()) + +if __name__ == "__main__": + main() +` + +const javaFileContent = ` +@Annotation +public class Test { + Test() { + // Do something here + } + + //Additional commenting + public static void sayHello() { + System.out.println("Hello, World!"); + } + + private void doSomething(int x, int y) throws Exception { + int z = x + y; + System.out.println("The sum of " + x + " and " + y + " is " + z); + } + + protected static int square(int x) { + return x * x; + } + + private static void manager(int a, int b) { + return a+b; + } + + public int ABCFUNCTION( int ABC, int PQR) { + return ABC + PQR; + } +}` diff --git a/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts new file mode 100644 index 00000000000..5694b33365d --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts @@ -0,0 +1,81 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { + JsonConfigFileNamingConvention, + checkLeftContextKeywordsForJson, + getPrefixSuffixOverlap, +} from 'aws-core-vscode/codewhisperer' + +describe('commonUtil', function () { + describe('getPrefixSuffixOverlap', function () { + it('Should return correct overlap', async function () { + assert.strictEqual(getPrefixSuffixOverlap('32rasdgvdsg', 'sg462ydfgbs'), `sg`) + assert.strictEqual(getPrefixSuffixOverlap('32rasdgbreh', 'brehsega'), `breh`) + assert.strictEqual(getPrefixSuffixOverlap('42y24hsd', '42y24hsdzqq23'), `42y24hsd`) + assert.strictEqual(getPrefixSuffixOverlap('ge23yt1', 'ge23yt1'), `ge23yt1`) + assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsa', 'a1sgdbsfbwsergs'), `a`) + assert.strictEqual(getPrefixSuffixOverlap('xxa', 'xa'), `xa`) + }) + + it('Should return empty overlap for prefix suffix not matching cases', async function () { + assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsa', '1sgdbsfbwsergs'), ``) + assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsab', '1sgdbsfbwsergs'), ``) + assert.strictEqual(getPrefixSuffixOverlap('2135t12', 'v2135t12'), ``) + assert.strictEqual(getPrefixSuffixOverlap('2135t12', 'zv2135t12'), ``) + assert.strictEqual(getPrefixSuffixOverlap('xa', 'xxa'), ``) + }) + + it('Should return empty overlap for empty string input', async function () { + assert.strictEqual(getPrefixSuffixOverlap('ergwsghws', ''), ``) + assert.strictEqual(getPrefixSuffixOverlap('', 'asfegw4eh'), ``) + }) + }) + + describe('checkLeftContextKeywordsForJson', function () { + it('Should return true for valid left context keywords', async function () { + assert.strictEqual( + checkLeftContextKeywordsForJson('foo.json', 'Create an S3 Bucket named CodeWhisperer', 'json'), + true + ) + }) + it('Should return false for invalid left context keywords', async function () { + assert.strictEqual( + checkLeftContextKeywordsForJson( + 'foo.json', + 'Create an S3 Bucket named CodeWhisperer in Cloudformation', + 'json' + ), + false + ) + }) + + for (const jsonConfigFile of JsonConfigFileNamingConvention) { + it(`should evalute by filename ${jsonConfigFile}`, function () { + assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile, 'foo', 'json'), false) + + assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile.toUpperCase(), 'bar', 'json'), false) + + assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile.toUpperCase(), 'baz', 'json'), false) + }) + + const upperCaseFilename = jsonConfigFile.toUpperCase() + it(`should evalute by filename and case insensitive ${upperCaseFilename}`, function () { + assert.strictEqual(checkLeftContextKeywordsForJson(upperCaseFilename, 'foo', 'json'), false) + + assert.strictEqual( + checkLeftContextKeywordsForJson(upperCaseFilename.toUpperCase(), 'bar', 'json'), + false + ) + + assert.strictEqual( + checkLeftContextKeywordsForJson(upperCaseFilename.toUpperCase(), 'baz', 'json'), + false + ) + }) + } + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts new file mode 100644 index 00000000000..4c2ca1190ca --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts @@ -0,0 +1,417 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as FakeTimers from '@sinonjs/fake-timers' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import * as crossFile from 'aws-core-vscode/codewhisperer' +import { + aLongStringWithLineCount, + aStringWithLineCount, + createMockTextEditor, + installFakeClock, +} from 'aws-core-vscode/test' +import { FeatureConfigProvider, crossFileContextConfig } from 'aws-core-vscode/codewhisperer' +import { + assertTabCount, + closeAllEditors, + createTestWorkspaceFolder, + toTextEditor, + shuffleList, + toFile, +} from 'aws-core-vscode/test' +import { areEqual, normalize } from 'aws-core-vscode/shared' +import * as path from 'path' + +let tempFolder: string + +describe('crossFileContextUtil', function () { + const fakeCancellationToken: vscode.CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: sinon.spy(), + } + + let mockEditor: vscode.TextEditor + let clock: FakeTimers.InstalledClock + + before(function () { + clock = installFakeClock() + }) + + after(function () { + clock.uninstall() + }) + + afterEach(function () { + sinon.restore() + }) + + describe('fetchSupplementalContextForSrc', function () { + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + afterEach(async function () { + sinon.restore() + }) + + it.skip('for control group, should return opentabs context where there will be 3 chunks and each chunk should contains 50 lines', async function () { + sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') + await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) + const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { + preview: false, + }) + + await assertTabCount(2) + + const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken) + assert.ok(actual) + assert.strictEqual(actual.supplementalContextItems.length, 3) + assert.strictEqual(actual.supplementalContextItems[0].content.split('\n').length, 50) + assert.strictEqual(actual.supplementalContextItems[1].content.split('\n').length, 50) + assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50) + }) + + it('for t1 group, should return repomap + opentabs context, should not exceed 20k total length', async function () { + await toTextEditor(aLongStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) + const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { + preview: false, + }) + + await assertTabCount(2) + + sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('t1') + + const mockLanguageClient = { + sendRequest: sinon.stub().resolves([ + { + content: 'foo'.repeat(3000), + score: 0, + filePath: 'q-inline', + }, + ]), + } as any + + const actual = await crossFile.fetchSupplementalContextForSrc( + myCurrentEditor, + fakeCancellationToken, + mockLanguageClient + ) + assert.ok(actual) + assert.strictEqual(actual.supplementalContextItems.length, 3) + assert.strictEqual(actual?.strategy, 'codemap') + assert.deepEqual(actual?.supplementalContextItems[0], { + content: 'foo'.repeat(3000), + score: 0, + filePath: 'q-inline', + }) + assert.strictEqual(actual.supplementalContextItems[1].content.split('\n').length, 50) + assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50) + }) + + it.skip('for t2 group, should return global bm25 context and no repomap', async function () { + await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) + const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { + preview: false, + }) + + await assertTabCount(2) + + sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('t2') + + const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken) + assert.ok(actual) + assert.strictEqual(actual.supplementalContextItems.length, 5) + assert.strictEqual(actual?.strategy, 'bm25') + + assert.deepEqual(actual?.supplementalContextItems[0], { + content: 'foo', + score: 5, + filePath: 'foo.java', + }) + + assert.deepEqual(actual?.supplementalContextItems[1], { + content: 'bar', + score: 4, + filePath: 'bar.java', + }) + assert.deepEqual(actual?.supplementalContextItems[2], { + content: 'baz', + score: 3, + filePath: 'baz.java', + }) + + assert.deepEqual(actual?.supplementalContextItems[3], { + content: 'qux', + score: 2, + filePath: 'qux.java', + }) + + assert.deepEqual(actual?.supplementalContextItems[4], { + content: 'quux', + score: 1, + filePath: 'quux.java', + }) + }) + }) + + describe('non supported language should return undefined', function () { + it('c++', async function () { + mockEditor = createMockTextEditor('content', 'fileName', 'cpp') + const actual = await crossFile.fetchSupplementalContextForSrc(mockEditor, fakeCancellationToken) + assert.strictEqual(actual, undefined) + }) + + it('ruby', async function () { + mockEditor = createMockTextEditor('content', 'fileName', 'ruby') + + const actual = await crossFile.fetchSupplementalContextForSrc(mockEditor, fakeCancellationToken) + + assert.strictEqual(actual, undefined) + }) + }) + + describe('getCrossFileCandidate', function () { + before(async function () { + this.timeout(60000) + }) + + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + afterEach(async function () { + await closeAllEditors() + }) + + it('should return opened files, exclude test files and sorted ascendingly by file distance', async function () { + const targetFile = path.join('src', 'service', 'microService', 'CodeWhispererFileContextProvider.java') + const fileWithDistance3 = path.join('src', 'service', 'CodewhispererRecommendationService.java') + const fileWithDistance5 = path.join('src', 'util', 'CodeWhispererConstants.java') + const fileWithDistance6 = path.join('src', 'ui', 'popup', 'CodeWhispererPopupManager.java') + const fileWithDistance7 = path.join('src', 'ui', 'popup', 'components', 'CodeWhispererPopup.java') + const fileWithDistance8 = path.join( + 'src', + 'ui', + 'popup', + 'components', + 'actions', + 'AcceptRecommendationAction.java' + ) + const testFile1 = path.join('test', 'service', 'CodeWhispererFileContextProviderTest.java') + const testFile2 = path.join('test', 'ui', 'CodeWhispererPopupManagerTest.java') + + const expectedFilePaths = [ + fileWithDistance3, + fileWithDistance5, + fileWithDistance6, + fileWithDistance7, + fileWithDistance8, + ] + + const shuffledFilePaths = shuffleList(expectedFilePaths) + + for (const filePath of shuffledFilePaths) { + await toTextEditor('', filePath, tempFolder, { preview: false }) + } + + await toTextEditor('', testFile1, tempFolder, { preview: false }) + await toTextEditor('', testFile2, tempFolder, { preview: false }) + const editor = await toTextEditor('', targetFile, tempFolder, { preview: false }) + + await assertTabCount(shuffledFilePaths.length + 3) + + const actual = await crossFile.getCrossFileCandidates(editor) + + assert.ok(actual.length === 5) + for (const [index, actualFile] of actual.entries()) { + const expectedFile = path.join(tempFolder, expectedFilePaths[index]) + assert.strictEqual(normalize(expectedFile), normalize(actualFile)) + assert.ok(areEqual(tempFolder, actualFile, expectedFile)) + } + }) + }) + + describe.skip('partial support - control group', function () { + const fileExtLists: string[] = [] + + before(async function () { + this.timeout(60000) + }) + + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + afterEach(async function () { + await closeAllEditors() + }) + + for (const fileExt of fileExtLists) { + it('should be empty if userGroup is control', async function () { + const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) + await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) + + await assertTabCount(4) + + const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) + + assert.ok(actual && actual.supplementalContextItems.length === 0) + }) + } + }) + + describe.skip('partial support - crossfile group', function () { + const fileExtLists: string[] = [] + + before(async function () { + this.timeout(60000) + }) + + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + afterEach(async function () { + await closeAllEditors() + }) + + for (const fileExt of fileExtLists) { + it('should be non empty if usergroup is Crossfile', async function () { + const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) + await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) + + await assertTabCount(4) + + const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) + + assert.ok(actual && actual.supplementalContextItems.length !== 0) + }) + } + }) + + describe('full support', function () { + const fileExtLists = ['java', 'js', 'ts', 'py', 'tsx', 'jsx'] + + before(async function () { + this.timeout(60000) + }) + + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + afterEach(async function () { + sinon.restore() + await closeAllEditors() + }) + + for (const fileExt of fileExtLists) { + it(`supplemental context for file ${fileExt} should be non empty`, async function () { + sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') + const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) + await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) + + await assertTabCount(4) + + const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) + + assert.ok(actual && actual.supplementalContextItems.length !== 0) + }) + } + }) + + describe('splitFileToChunks', function () { + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + it('should split file to a chunk of 2 lines', async function () { + const filePath = path.join(tempFolder, 'file.txt') + await toFile('line_1\nline_2\nline_3\nline_4\nline_5\nline_6\nline_7', filePath) + + const chunks = await crossFile.splitFileToChunks(filePath, 2) + + assert.strictEqual(chunks.length, 4) + assert.strictEqual(chunks[0].content, 'line_1\nline_2') + assert.strictEqual(chunks[1].content, 'line_3\nline_4') + assert.strictEqual(chunks[2].content, 'line_5\nline_6') + assert.strictEqual(chunks[3].content, 'line_7') + }) + + it('should split file to a chunk of 5 lines', async function () { + const filePath = path.join(tempFolder, 'file.txt') + await toFile('line_1\nline_2\nline_3\nline_4\nline_5\nline_6\nline_7', filePath) + + const chunks = await crossFile.splitFileToChunks(filePath, 5) + + assert.strictEqual(chunks.length, 2) + assert.strictEqual(chunks[0].content, 'line_1\nline_2\nline_3\nline_4\nline_5') + assert.strictEqual(chunks[1].content, 'line_6\nline_7') + }) + + it('codewhisperer crossfile config should use 50 lines', async function () { + const filePath = path.join(tempFolder, 'file.txt') + await toFile(aStringWithLineCount(210), filePath) + + const chunks = await crossFile.splitFileToChunks(filePath, crossFileContextConfig.numberOfLinesEachChunk) + + // (210 / 50) + 1 + assert.strictEqual(chunks.length, 5) + // line0 -> line49 + assert.strictEqual(chunks[0].content, aStringWithLineCount(50, 0)) + // line50 -> line99 + assert.strictEqual(chunks[1].content, aStringWithLineCount(50, 50)) + // line100 -> line149 + assert.strictEqual(chunks[2].content, aStringWithLineCount(50, 100)) + // line150 -> line199 + assert.strictEqual(chunks[3].content, aStringWithLineCount(50, 150)) + // line 200 -> line209 + assert.strictEqual(chunks[4].content, aStringWithLineCount(10, 200)) + }) + + it('linkChunks should add another chunk which will link to the first chunk and chunk.nextContent should reflect correct value', async function () { + const filePath = path.join(tempFolder, 'file.txt') + await toFile(aStringWithLineCount(210), filePath) + + const chunks = await crossFile.splitFileToChunks(filePath, crossFileContextConfig.numberOfLinesEachChunk) + const linkedChunks = crossFile.linkChunks(chunks) + + // 210 / 50 + 2 + assert.strictEqual(linkedChunks.length, 6) + + // 0th + assert.strictEqual(linkedChunks[0].content, aStringWithLineCount(3, 0)) + assert.strictEqual(linkedChunks[0].nextContent, aStringWithLineCount(50, 0)) + + // 1st + assert.strictEqual(linkedChunks[1].content, aStringWithLineCount(50, 0)) + assert.strictEqual(linkedChunks[1].nextContent, aStringWithLineCount(50, 50)) + + // 2nd + assert.strictEqual(linkedChunks[2].content, aStringWithLineCount(50, 50)) + assert.strictEqual(linkedChunks[2].nextContent, aStringWithLineCount(50, 100)) + + // 3rd + assert.strictEqual(linkedChunks[3].content, aStringWithLineCount(50, 100)) + assert.strictEqual(linkedChunks[3].nextContent, aStringWithLineCount(50, 150)) + + // 4th + assert.strictEqual(linkedChunks[4].content, aStringWithLineCount(50, 150)) + assert.strictEqual(linkedChunks[4].nextContent, aStringWithLineCount(10, 200)) + + // 5th + assert.strictEqual(linkedChunks[5].content, aStringWithLineCount(10, 200)) + assert.strictEqual(linkedChunks[5].nextContent, aStringWithLineCount(10, 200)) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts b/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts new file mode 100644 index 00000000000..3875dbbd0f2 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts @@ -0,0 +1,392 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'assert' +import * as codewhispererClient from 'aws-core-vscode/codewhisperer' +import * as EditorContext from 'aws-core-vscode/codewhisperer' +import { + createMockDocument, + createMockTextEditor, + createMockClientRequest, + resetCodeWhispererGlobalVariables, + toTextEditor, + createTestWorkspaceFolder, + closeAllEditors, +} from 'aws-core-vscode/test' +import { globals } from 'aws-core-vscode/shared' +import { GenerateCompletionsRequest } from 'aws-core-vscode/codewhisperer' +import * as vscode from 'vscode' + +export function createNotebookCell( + document: vscode.TextDocument = createMockDocument('def example():\n return "test"'), + kind: vscode.NotebookCellKind = vscode.NotebookCellKind.Code, + notebook: vscode.NotebookDocument = {} as any, + index: number = 0, + outputs: vscode.NotebookCellOutput[] = [], + metadata: { readonly [key: string]: any } = {}, + executionSummary?: vscode.NotebookCellExecutionSummary +): vscode.NotebookCell { + return { + document, + kind, + notebook, + index, + outputs, + metadata, + executionSummary, + } +} + +describe('editorContext', function () { + let telemetryEnabledDefault: boolean + let tempFolder: string + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + telemetryEnabledDefault = globals.telemetry.telemetryEnabled + }) + + afterEach(async function () { + await globals.telemetry.setTelemetryEnabled(telemetryEnabledDefault) + }) + + describe('extractContextForCodeWhisperer', function () { + it('Should return expected context', function () { + const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) + const actual = EditorContext.extractContextForCodeWhisperer(editor) + const expected: codewhispererClient.FileContext = { + fileUri: 'file:///test.py', + filename: 'test.py', + programmingLanguage: { + languageName: 'python', + }, + leftFileContent: 'import math\ndef two_sum(nums,', + rightFileContent: ' target):\n', + } + assert.deepStrictEqual(actual, expected) + }) + + it('Should return expected context within max char limit', function () { + const editor = createMockTextEditor( + 'import math\ndef ' + 'a'.repeat(10340) + 'two_sum(nums, target):\n', + 'test.py', + 'python', + 1, + 17 + ) + const actual = EditorContext.extractContextForCodeWhisperer(editor) + const expected: codewhispererClient.FileContext = { + fileUri: 'file:///test.py', + filename: 'test.py', + programmingLanguage: { + languageName: 'python', + }, + leftFileContent: 'import math\ndef aaaaaaaaaaaaa', + rightFileContent: 'a'.repeat(10240), + } + assert.deepStrictEqual(actual, expected) + }) + + it('in a notebook, includes context from other cells', async function () { + const cells: vscode.NotebookCellData[] = [ + new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, 'Previous cell', 'python'), + new vscode.NotebookCellData( + vscode.NotebookCellKind.Code, + 'import numpy as np\nimport pandas as pd\n\ndef analyze_data(df):\n # Current cell with cursor here', + 'python' + ), + new vscode.NotebookCellData( + vscode.NotebookCellKind.Code, + '# Process the data\nresult = analyze_data(df)\nprint(result)', + 'python' + ), + ] + + const document = await vscode.workspace.openNotebookDocument( + 'jupyter-notebook', + new vscode.NotebookData(cells) + ) + const editor: any = { + document: document.cellAt(1).document, + selection: { active: new vscode.Position(4, 13) }, + } + + const actual = EditorContext.extractContextForCodeWhisperer(editor) + const expected: codewhispererClient.FileContext = { + fileUri: editor.document.uri.toString(), + filename: 'Untitled-1.py', + programmingLanguage: { + languageName: 'python', + }, + leftFileContent: + '# Previous cell\nimport numpy as np\nimport pandas as pd\n\ndef analyze_data(df):\n # Current', + rightFileContent: + ' cell with cursor here\n# Process the data\nresult = analyze_data(df)\nprint(result)\n', + } + assert.deepStrictEqual(actual, expected) + }) + }) + + describe('getFileName', function () { + it('Should return expected filename given a document reading test.py', function () { + const editor = createMockTextEditor('', 'test.py', 'python', 1, 17) + const actual = EditorContext.getFileName(editor) + const expected = 'test.py' + assert.strictEqual(actual, expected) + }) + + it('Should return expected filename for a long filename', async function () { + const editor = createMockTextEditor('', 'a'.repeat(1500), 'python', 1, 17) + const actual = EditorContext.getFileName(editor) + const expected = 'a'.repeat(1024) + assert.strictEqual(actual, expected) + }) + }) + + describe('getFileRelativePath', function () { + this.beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + it('Should return a new filename with correct extension given a .ipynb file', function () { + const languageToExtension = new Map([ + ['python', 'py'], + ['rust', 'rs'], + ['javascript', 'js'], + ['typescript', 'ts'], + ['c', 'c'], + ]) + + for (const [language, extension] of languageToExtension.entries()) { + const editor = createMockTextEditor('', 'test.ipynb', language, 1, 17) + const actual = EditorContext.getFileRelativePath(editor) + const expected = 'test.' + extension + assert.strictEqual(actual, expected) + } + }) + + it('Should return relative path', async function () { + const editor = await toTextEditor('tttt', 'test.py', tempFolder) + const actual = EditorContext.getFileRelativePath(editor) + const expected = 'test.py' + assert.strictEqual(actual, expected) + }) + + afterEach(async function () { + await closeAllEditors() + }) + }) + + describe('getNotebookCellContext', function () { + it('Should return cell text for python code cells when language is python', function () { + const mockCodeCell = createNotebookCell(createMockDocument('def example():\n return "test"')) + const result = EditorContext.getNotebookCellContext(mockCodeCell, 'python') + assert.strictEqual(result, 'def example():\n return "test"') + }) + + it('Should return java comments for python code cells when language is java', function () { + const mockCodeCell = createNotebookCell(createMockDocument('def example():\n return "test"')) + const result = EditorContext.getNotebookCellContext(mockCodeCell, 'java') + assert.strictEqual(result, '// def example():\n// return "test"') + }) + + it('Should return python comments for java code cells when language is python', function () { + const mockCodeCell = createNotebookCell(createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java')) + const result = EditorContext.getNotebookCellContext(mockCodeCell, 'python') + assert.strictEqual(result, '# println(1 + 1);') + }) + + it('Should add python comment prefixes for markdown cells when language is python', function () { + const mockMarkdownCell = createNotebookCell( + createMockDocument('# Heading\nThis is a markdown cell'), + vscode.NotebookCellKind.Markup + ) + const result = EditorContext.getNotebookCellContext(mockMarkdownCell, 'python') + assert.strictEqual(result, '# # Heading\n# This is a markdown cell') + }) + + it('Should add java comment prefixes for markdown cells when language is java', function () { + const mockMarkdownCell = createNotebookCell( + createMockDocument('# Heading\nThis is a markdown cell'), + vscode.NotebookCellKind.Markup + ) + const result = EditorContext.getNotebookCellContext(mockMarkdownCell, 'java') + assert.strictEqual(result, '// # Heading\n// This is a markdown cell') + }) + }) + + describe('getNotebookCellsSliceContext', function () { + it('Should extract content from cells in reverse order up to maxLength from prefix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('First cell content')), + createNotebookCell(createMockDocument('Second cell content')), + createNotebookCell(createMockDocument('Third cell content')), + ] + + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', false) + assert.strictEqual(result, 'First cell content\nSecond cell content\nThird cell content\n') + }) + + it('Should extract content from cells in reverse order up to maxLength from suffix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('First cell content')), + createNotebookCell(createMockDocument('Second cell content')), + createNotebookCell(createMockDocument('Third cell content')), + ] + + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', true) + assert.strictEqual(result, 'First cell content\nSecond cell content\nThird cell content\n') + }) + + it('Should respect maxLength parameter from prefix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('First')), + createNotebookCell(createMockDocument('Second')), + createNotebookCell(createMockDocument('Third')), + createNotebookCell(createMockDocument('Fourth')), + ] + // Should only include part of second cell and the last two cells + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 15, 'python', false) + assert.strictEqual(result, 'd\nThird\nFourth\n') + }) + + it('Should respect maxLength parameter from suffix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('First')), + createNotebookCell(createMockDocument('Second')), + createNotebookCell(createMockDocument('Third')), + createNotebookCell(createMockDocument('Fourth')), + ] + + // Should only include first cell and part of second cell + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 15, 'python', true) + assert.strictEqual(result, 'First\nSecond\nTh') + }) + + it('Should handle empty cells array from prefix cells', function () { + const result = EditorContext.getNotebookCellsSliceContext([], 100, 'python', false) + assert.strictEqual(result, '') + }) + + it('Should handle empty cells array from suffix cells', function () { + const result = EditorContext.getNotebookCellsSliceContext([], 100, 'python', true) + assert.strictEqual(result, '') + }) + + it('Should add python comments to markdown prefix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), + createNotebookCell(createMockDocument('def example():\n return "test"')), + ] + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', false) + assert.strictEqual(result, '# # Heading\n# This is markdown\ndef example():\n return "test"\n') + }) + + it('Should add python comments to markdown suffix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), + createNotebookCell(createMockDocument('def example():\n return "test"')), + ] + + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', true) + assert.strictEqual(result, '# # Heading\n# This is markdown\ndef example():\n return "test"\n') + }) + + it('Should add java comments to markdown and python prefix cells when language is java', function () { + const mockCells = [ + createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), + createNotebookCell(createMockDocument('def example():\n return "test"')), + ] + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'java', false) + assert.strictEqual(result, '// # Heading\n// This is markdown\n// def example():\n// return "test"\n') + }) + + it('Should add java comments to markdown and python suffix cells when language is java', function () { + const mockCells = [ + createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), + createNotebookCell(createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java')), + ] + + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'java', true) + assert.strictEqual(result, '// # Heading\n// This is markdown\nprintln(1 + 1);\n') + }) + + it('Should handle code prefix cells with different languages', function () { + const mockCells = [ + createNotebookCell( + createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java'), + vscode.NotebookCellKind.Code + ), + createNotebookCell(createMockDocument('def example():\n return "test"')), + ] + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', false) + assert.strictEqual(result, '# println(1 + 1);\ndef example():\n return "test"\n') + }) + + it('Should handle code suffix cells with different languages', function () { + const mockCells = [ + createNotebookCell( + createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java'), + vscode.NotebookCellKind.Code + ), + createNotebookCell(createMockDocument('def example():\n return "test"')), + ] + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', true) + assert.strictEqual(result, '# println(1 + 1);\ndef example():\n return "test"\n') + }) + }) + + describe('validateRequest', function () { + it('Should return false if request filename.length is invalid', function () { + const req = createMockClientRequest() + req.fileContext.filename = '' + assert.ok(!EditorContext.validateRequest(req)) + }) + + it('Should return false if request programming language is invalid', function () { + const req = createMockClientRequest() + req.fileContext.programmingLanguage.languageName = '' + assert.ok(!EditorContext.validateRequest(req)) + req.fileContext.programmingLanguage.languageName = 'a'.repeat(200) + assert.ok(!EditorContext.validateRequest(req)) + }) + + it('Should return false if request left or right context exceeds max length', function () { + const req = createMockClientRequest() + req.fileContext.leftFileContent = 'a'.repeat(256000) + assert.ok(!EditorContext.validateRequest(req)) + req.fileContext.leftFileContent = 'a' + req.fileContext.rightFileContent = 'a'.repeat(256000) + assert.ok(!EditorContext.validateRequest(req)) + }) + + it('Should return true if above conditions are not met', function () { + const req = createMockClientRequest() + assert.ok(EditorContext.validateRequest(req)) + }) + }) + + describe('getLeftContext', function () { + it('Should return expected left context', function () { + const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) + const actual = EditorContext.getLeftContext(editor, 1) + const expected = '...wo_sum(nums, target)' + assert.strictEqual(actual, expected) + }) + }) + + describe('buildListRecommendationRequest', function () { + it('Should return expected fields for optOut, nextToken and reference config', async function () { + const nextToken = 'testToken' + const optOutPreference = false + await globals.telemetry.setTelemetryEnabled(false) + const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) + const actual = await EditorContext.buildListRecommendationRequest(editor, nextToken, optOutPreference) + + assert.strictEqual(actual.request.nextToken, nextToken) + assert.strictEqual((actual.request as GenerateCompletionsRequest).optOutPreference, 'OPTOUT') + assert.strictEqual(actual.request.referenceTrackerConfiguration?.recommendationsWithReferences, 'BLOCK') + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts new file mode 100644 index 00000000000..24062a81b7c --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts @@ -0,0 +1,42 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' +import { getLogger } from 'aws-core-vscode/shared' +import { resetIntelliSenseState, vsCodeState } from 'aws-core-vscode/codewhisperer' + +describe('globalStateUtil', function () { + let loggerSpy: sinon.SinonSpy + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + vsCodeState.isIntelliSenseActive = true + loggerSpy = sinon.spy(getLogger(), 'info') + }) + + this.afterEach(function () { + sinon.restore() + }) + + it('Should skip when CodeWhisperer is turned off', async function () { + const isManualTriggerEnabled = false + const isAutomatedTriggerEnabled = false + resetIntelliSenseState(isManualTriggerEnabled, isAutomatedTriggerEnabled, true) + assert.ok(!loggerSpy.called) + }) + + it('Should skip when invocationContext is not active', async function () { + vsCodeState.isIntelliSenseActive = false + resetIntelliSenseState(false, false, true) + assert.ok(!loggerSpy.called) + }) + + it('Should skip when no valid recommendations', async function () { + resetIntelliSenseState(true, true, false) + assert.ok(!loggerSpy.called) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts new file mode 100644 index 00000000000..cf2fd151262 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts @@ -0,0 +1,254 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as FakeTimers from '@sinonjs/fake-timers' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import * as os from 'os' +import * as crossFile from 'aws-core-vscode/codewhisperer' +import { TestFolder, assertTabCount, installFakeClock } from 'aws-core-vscode/test' +import { CodeWhispererSupplementalContext, FeatureConfigProvider } from 'aws-core-vscode/codewhisperer' +import { toTextEditor } from 'aws-core-vscode/test' + +const newLine = os.EOL + +describe('supplementalContextUtil', function () { + let testFolder: TestFolder + let clock: FakeTimers.InstalledClock + + const fakeCancellationToken: vscode.CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: sinon.spy(), + } + + before(function () { + clock = installFakeClock() + }) + + after(function () { + clock.uninstall() + }) + + beforeEach(async function () { + testFolder = await TestFolder.create() + sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') + }) + + afterEach(function () { + sinon.restore() + }) + + describe('fetchSupplementalContext', function () { + describe('openTabsContext', function () { + it('opentabContext should include chunks if non empty', async function () { + await toTextEditor('class Foo', 'Foo.java', testFolder.path, { preview: false }) + await toTextEditor('class Bar', 'Bar.java', testFolder.path, { preview: false }) + await toTextEditor('class Baz', 'Baz.java', testFolder.path, { preview: false }) + + const editor = await toTextEditor('public class Foo {}', 'Query.java', testFolder.path, { + preview: false, + }) + + await assertTabCount(4) + + const actual = await crossFile.fetchSupplementalContext(editor, fakeCancellationToken) + assert.ok(actual?.supplementalContextItems.length === 3) + }) + + it('opentabsContext should filter out empty chunks', async function () { + // open 3 files as supplemental context candidate files but none of them have contents + await toTextEditor('', 'Foo.java', testFolder.path, { preview: false }) + await toTextEditor('', 'Bar.java', testFolder.path, { preview: false }) + await toTextEditor('', 'Baz.java', testFolder.path, { preview: false }) + + const editor = await toTextEditor('public class Foo {}', 'Query.java', testFolder.path, { + preview: false, + }) + + await assertTabCount(4) + + const actual = await crossFile.fetchSupplementalContext(editor, fakeCancellationToken) + assert.ok(actual?.supplementalContextItems.length === 0) + }) + }) + }) + + describe('truncation', function () { + it('truncate context should do nothing if everything fits in constraint', function () { + const chunkA: crossFile.CodeWhispererSupplementalContextItem = { + content: 'a', + filePath: 'a.java', + score: 0, + } + const chunkB: crossFile.CodeWhispererSupplementalContextItem = { + content: 'b', + filePath: 'b.java', + score: 1, + } + const chunks = [chunkA, chunkB] + + const supplementalContext: CodeWhispererSupplementalContext = { + isUtg: false, + isProcessTimeout: false, + supplementalContextItems: chunks, + contentsLength: 25000, + latency: 0, + strategy: 'codemap', + } + + const actual = crossFile.truncateSuppelementalContext(supplementalContext) + assert.strictEqual(actual.supplementalContextItems.length, 2) + assert.strictEqual(actual.supplementalContextItems[0].content, 'a') + assert.strictEqual(actual.supplementalContextItems[1].content, 'b') + }) + + it('truncateLineByLine should drop the last line if max length is greater than threshold', function () { + const input = + repeatString('a', 11) + + newLine + + repeatString('b', 11) + + newLine + + repeatString('c', 11) + + newLine + + repeatString('d', 11) + + newLine + + repeatString('e', 11) + + assert.ok(input.length > 50) + const actual = crossFile.truncateLineByLine(input, 50) + assert.ok(actual.length <= 50) + + const input2 = repeatString(`b${newLine}`, 10) + const actual2 = crossFile.truncateLineByLine(input2, 8) + assert.ok(actual2.length <= 8) + }) + + it('truncation context should make context length per item lte 10240 cap', function () { + const chunkA: crossFile.CodeWhispererSupplementalContextItem = { + content: repeatString(`a${newLine}`, 4000), + filePath: 'a.java', + score: 0, + } + const chunkB: crossFile.CodeWhispererSupplementalContextItem = { + content: repeatString(`b${newLine}`, 6000), + filePath: 'b.java', + score: 1, + } + const chunkC: crossFile.CodeWhispererSupplementalContextItem = { + content: repeatString(`c${newLine}`, 1000), + filePath: 'c.java', + score: 2, + } + const chunkD: crossFile.CodeWhispererSupplementalContextItem = { + content: repeatString(`d${newLine}`, 1500), + filePath: 'd.java', + score: 3, + } + + assert.ok( + chunkA.content.length + chunkB.content.length + chunkC.content.length + chunkD.content.length > 20480 + ) + + const supplementalContext: CodeWhispererSupplementalContext = { + isUtg: false, + isProcessTimeout: false, + supplementalContextItems: [chunkA, chunkB, chunkC, chunkD], + contentsLength: 25000, + latency: 0, + strategy: 'codemap', + } + + const actual = crossFile.truncateSuppelementalContext(supplementalContext) + assert.strictEqual(actual.supplementalContextItems.length, 3) + assert.ok(actual.contentsLength <= 20480) + assert.strictEqual(actual.strategy, 'codemap') + }) + + it('truncate context should make context items lte 5', function () { + const chunkA: crossFile.CodeWhispererSupplementalContextItem = { + content: 'a', + filePath: 'a.java', + score: 0, + } + const chunkB: crossFile.CodeWhispererSupplementalContextItem = { + content: 'b', + filePath: 'b.java', + score: 1, + } + const chunkC: crossFile.CodeWhispererSupplementalContextItem = { + content: 'c', + filePath: 'c.java', + score: 2, + } + const chunkD: crossFile.CodeWhispererSupplementalContextItem = { + content: 'd', + filePath: 'd.java', + score: 3, + } + const chunkE: crossFile.CodeWhispererSupplementalContextItem = { + content: 'e', + filePath: 'e.java', + score: 4, + } + const chunkF: crossFile.CodeWhispererSupplementalContextItem = { + content: 'f', + filePath: 'f.java', + score: 5, + } + const chunkG: crossFile.CodeWhispererSupplementalContextItem = { + content: 'g', + filePath: 'g.java', + score: 6, + } + const chunks = [chunkA, chunkB, chunkC, chunkD, chunkE, chunkF, chunkG] + + assert.strictEqual(chunks.length, 7) + + const supplementalContext: CodeWhispererSupplementalContext = { + isUtg: false, + isProcessTimeout: false, + supplementalContextItems: chunks, + contentsLength: 25000, + latency: 0, + strategy: 'codemap', + } + + const actual = crossFile.truncateSuppelementalContext(supplementalContext) + assert.strictEqual(actual.supplementalContextItems.length, 5) + }) + + describe('truncate line by line', function () { + it('should return empty if empty string is provided', function () { + const input = '' + const actual = crossFile.truncateLineByLine(input, 50) + assert.strictEqual(actual, '') + }) + + it('should return empty if 0 max length is provided', function () { + const input = 'aaaaa' + const actual = crossFile.truncateLineByLine(input, 0) + assert.strictEqual(actual, '') + }) + + it('should flip the value if negative max length is provided', function () { + const input = `aaaaa${newLine}bbbbb` + const actual = crossFile.truncateLineByLine(input, -6) + const expected = crossFile.truncateLineByLine(input, 6) + assert.strictEqual(actual, expected) + assert.strictEqual(actual, 'aaaaa') + }) + }) + }) +}) + +function repeatString(s: string, n: number): string { + let output = '' + for (let i = 0; i < n; i++) { + output += s + } + + return output +} diff --git a/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts b/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts new file mode 100644 index 00000000000..67359b8a6fc --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts @@ -0,0 +1,63 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as utgUtils from 'aws-core-vscode/codewhisperer' + +describe('shouldFetchUtgContext', () => { + it('fully supported language', function () { + assert.ok(utgUtils.shouldFetchUtgContext('java')) + }) + + it('partially supported language', () => { + assert.strictEqual(utgUtils.shouldFetchUtgContext('python'), false) + }) + + it('not supported language', () => { + assert.strictEqual(utgUtils.shouldFetchUtgContext('typescript'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('javascript'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('javascriptreact'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('typescriptreact'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('scala'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('shellscript'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('csharp'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('c'), undefined) + }) +}) + +describe('guessSrcFileName', function () { + it('should return undefined if no matching regex', function () { + assert.strictEqual(utgUtils.guessSrcFileName('Foo.java', 'java'), undefined) + assert.strictEqual(utgUtils.guessSrcFileName('folder1/foo.py', 'python'), undefined) + assert.strictEqual(utgUtils.guessSrcFileName('Bar.js', 'javascript'), undefined) + }) + + it('java', function () { + assert.strictEqual(utgUtils.guessSrcFileName('FooTest.java', 'java'), 'Foo.java') + assert.strictEqual(utgUtils.guessSrcFileName('FooTests.java', 'java'), 'Foo.java') + }) + + it('python', function () { + assert.strictEqual(utgUtils.guessSrcFileName('test_foo.py', 'python'), 'foo.py') + assert.strictEqual(utgUtils.guessSrcFileName('foo_test.py', 'python'), 'foo.py') + }) + + it('typescript', function () { + assert.strictEqual(utgUtils.guessSrcFileName('Foo.test.ts', 'typescript'), 'Foo.ts') + assert.strictEqual(utgUtils.guessSrcFileName('Foo.spec.ts', 'typescript'), 'Foo.ts') + }) + + it('javascript', function () { + assert.strictEqual(utgUtils.guessSrcFileName('Foo.test.js', 'javascript'), 'Foo.js') + assert.strictEqual(utgUtils.guessSrcFileName('Foo.spec.js', 'javascript'), 'Foo.js') + }) +}) diff --git a/packages/core/package.json b/packages/core/package.json index 7be37423006..efc06d6e6de 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -323,124 +323,173 @@ "fontCharacter": "\\f1d2" } }, - "aws-lambda-function": { + "aws-lambda-deployed-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d3" } }, - "aws-mynah-MynahIconBlack": { + "aws-lambda-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d4" } }, - "aws-mynah-MynahIconWhite": { + "aws-lambda-invoke-remotely": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d5" } }, - "aws-mynah-logo": { + "aws-mynah-MynahIconBlack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d6" } }, - "aws-redshift-cluster": { + "aws-mynah-MynahIconWhite": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d7" } }, - "aws-redshift-cluster-connected": { + "aws-mynah-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d8" } }, - "aws-redshift-database": { + "aws-redshift-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d9" } }, - "aws-redshift-redshift-cluster-connected": { + "aws-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1da" } }, - "aws-redshift-schema": { + "aws-redshift-database": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1db" } }, - "aws-redshift-table": { + "aws-redshift-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1dc" } }, - "aws-s3-bucket": { + "aws-redshift-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1dd" } }, - "aws-s3-create-bucket": { + "aws-redshift-table": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1de" } }, - "aws-sagemaker-code-editor": { + "aws-s3-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1df" } }, - "aws-sagemaker-jupyter-lab": { + "aws-s3-create-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e0" } }, - "aws-schemas-registry": { + "aws-sagemaker-code-editor": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e1" } }, - "aws-schemas-schema": { + "aws-sagemaker-jupyter-lab": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e2" } }, - "aws-stepfunctions-preview": { + "aws-sagemakerunifiedstudio-catalog": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e3" } + }, + "aws-sagemakerunifiedstudio-spaces": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e4" + } + }, + "aws-sagemakerunifiedstudio-spaces-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e5" + } + }, + "aws-sagemakerunifiedstudio-symbol-int": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e6" + } + }, + "aws-sagemakerunifiedstudio-table": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e7" + } + }, + "aws-schemas-registry": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e8" + } + }, + "aws-schemas-schema": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e9" + } + }, + "aws-stepfunctions-preview": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1ea" + } } } }, @@ -498,12 +547,13 @@ "@types/sinon": "^10.0.5", "@types/sinonjs__fake-timers": "^8.1.2", "@types/stream-buffers": "^3.0.7", + "@types/svgdom": "^0.1.2", "@types/tcp-port-used": "^1.0.1", "@types/uuid": "^9.0.1", "@types/whatwg-url": "^11.0.4", "@types/xml2js": "^0.4.11", - "@types/svgdom": "^0.1.2", "@vue/compiler-sfc": "^3.3.2", + "aws-sdk-client-mock": "^4.1.0", "c8": "^9.0.0", "circular-dependency-plugin": "^5.2.2", "css-loader": "^6.10.0", @@ -531,7 +581,8 @@ "@amzn/amazon-q-developer-streaming-client": "file:../../src.gen/@amzn/amazon-q-developer-streaming-client", "@amzn/codewhisperer-streaming": "file:../../src.gen/@amzn/codewhisperer-streaming", "@amzn/sagemaker-client": "file:../../src.gen/@amzn/sagemaker-client/1.0.0.tgz", - "@aws-sdk/credential-providers": "<3.731.0", + "@amzn/glue-catalog-client": "file:../../src.gen/@amzn/glue-catalog-client/0.0.1.tgz", + "@aws-sdk/client-accessanalyzer": "^3.888.0", "@aws-sdk/client-api-gateway": "<3.731.0", "@aws-sdk/client-apprunner": "<3.731.0", "@aws-sdk/client-cloudcontrol": "<3.731.0", @@ -539,20 +590,34 @@ "@aws-sdk/client-cloudwatch-logs": "<3.731.0", "@aws-sdk/client-codecatalyst": "<3.731.0", "@aws-sdk/client-cognito-identity": "<3.731.0", + "@aws-sdk/client-datazone": "^3.848.0", "@aws-sdk/client-docdb": "<3.731.0", "@aws-sdk/client-docdb-elastic": "<3.731.0", "@aws-sdk/client-ec2": "<3.731.0", + "@aws-sdk/client-ecr": "~3.693.0", + "@aws-sdk/client-ecs": "~3.693.0", + "@aws-sdk/client-eks": "^3.583.0", + "@aws-sdk/client-glue": "^3.852.0", "@aws-sdk/client-iam": "<3.731.0", + "@aws-sdk/client-iot": "~3.693.0", + "@aws-sdk/client-iotsecuretunneling": "~3.693.0", "@aws-sdk/client-lambda": "<3.731.0", + "@aws-sdk/client-redshift": "~3.693.0", + "@aws-sdk/client-redshift-data": "~3.693.0", + "@aws-sdk/client-redshift-serverless": "~3.693.0", "@aws-sdk/client-s3": "<3.731.0", + "@aws-sdk/client-s3-control": "^3.830.0", "@aws-sdk/client-sagemaker": "<3.696.0", + "@aws-sdk/client-schemas": "~3.693.0", + "@aws-sdk/client-secrets-manager": "~3.693.0", + "@aws-sdk/client-sfn": "<3.731.0", "@aws-sdk/client-ssm": "<3.731.0", "@aws-sdk/client-sso": "<3.731.0", "@aws-sdk/client-sso-oidc": "<3.731.0", - "@aws-sdk/client-sfn": "<3.731.0", "@aws-sdk/credential-provider-env": "<3.731.0", "@aws-sdk/credential-provider-process": "<3.731.0", "@aws-sdk/credential-provider-sso": "<3.731.0", + "@aws-sdk/credential-providers": "<3.731.0", "@aws-sdk/lib-storage": "<3.731.0", "@aws-sdk/property-provider": "<3.731.0", "@aws-sdk/protocol-http": "<3.731.0", @@ -562,6 +627,7 @@ "@aws/mynah-ui": "^4.35.4", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", + "@kubernetes/client-node": "^0.20.0", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/middleware-retry": "^4.0.3", "@smithy/node-http-handler": "^4.0.2", @@ -569,6 +635,7 @@ "@smithy/service-error-classification": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.0", "@smithy/util-retry": "^4.0.1", + "@svgdotjs/svg.js": "^3.0.16", "@vscode/debugprotocol": "^1.57.0", "@zip.js/zip.js": "^2.7.41", "adm-zip": "^0.5.10", @@ -587,6 +654,7 @@ "http2": "^3.3.6", "i18n-ts": "^1.0.5", "immutable": "^4.3.0", + "jaro-winkler": "^0.2.8", "jose": "5.4.1", "js-yaml": "^4.1.0", "jsonc-parser": "^3.2.0", @@ -596,12 +664,14 @@ "mime-types": "^2.1.32", "node-fetch": "^2.7.0", "portfinder": "^1.0.32", + "protobufjs": "^7.2.6", "semver": "^7.5.4", "stream-buffers": "^3.0.2", "strip-ansi": "^5.2.0", + "svgdom": "^0.1.0", "tcp-port-used": "^1.0.1", - "vscode-languageclient": "^6.1.4", - "vscode-languageserver": "^6.1.1", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.15.3", "vscode-languageserver-textdocument": "^1.0.8", "vue": "^3.3.4", @@ -611,11 +681,7 @@ "winston-transport": "^4.6.0", "ws": "^8.16.0", "xml2js": "^0.6.1", - "yaml-cfn": "^0.3.2", - "protobufjs": "^7.2.6", - "@svgdotjs/svg.js": "^3.0.16", - "svgdom": "^0.1.0", - "jaro-winkler": "^0.2.8" + "yaml-cfn": "^0.3.2" }, "overrides": { "webfont": { diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 06343f17c75..bda0d97b022 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -99,6 +99,7 @@ "AWS.configuration.description.amazonq.workspaceIndexCacheDirPath": "The path to the directory that contains the cache of the index of your workspace files", "AWS.configuration.description.amazonq.ignoredSecurityIssues": "Specifies a list of code issue identifiers that Amazon Q should ignore when reviewing your workspace. Each item in the array should be a unique string identifier for a specific code issue. This allows you to suppress notifications for known issues that you've assessed and determined to be false positives or not applicable to your project. Use this setting with caution, as it may cause you to miss important security alerts.", "AWS.configuration.description.amazonq.proxy.certificateAuthority": "Path to a Certificate Authority (PEM file) for SSL/TLS verification when using a proxy.", + "AWS.configuration.description.amazonq.proxy.enableProxyAndCertificateAutoDiscovery": "Automatically detect system proxy settings and SSL certificates.", "AWS.command.apig.invokeRemoteRestApi": "Invoke remotely", "AWS.command.apig.invokeRemoteRestApi.cn": "Invoke on Amazon", "AWS.appBuilder.explorerTitle": "Application Builder", @@ -144,8 +145,6 @@ "AWS.command.amazonq.optimizeCode": "Optimize", "AWS.command.amazonq.sendToPrompt": "Send to prompt", "AWS.command.amazonq.generateUnitTests": "Generate Tests", - "AWS.command.amazonq.security.scan": "Run Project Review", - "AWS.command.amazonq.security.fileScan": "Run File Review", "AWS.command.amazonq.generateFix": "Fix", "AWS.command.amazonq.viewDetails": "View Details", "AWS.command.amazonq.explainIssue": "Explain", @@ -230,6 +229,10 @@ "AWS.command.s3.createFolder": "Create Folder...", "AWS.command.s3.uploadFile": "Upload Files...", "AWS.command.s3.uploadFileToParent": "Upload to Parent...", + "AWS.command.smus.switchProject": "Switch Project", + "AWS.command.smus.refreshProject": "Refresh Project", + "AWS.command.smus.refresh": "Refresh", + "AWS.command.smus.signOut": "Sign Out", "AWS.command.sagemaker.filterSpaces": "Filter Sagemaker Spaces", "AWS.command.stepFunctions.createStateMachineFromTemplate": "Create a new Step Functions state machine", "AWS.command.stepFunctions.publishStateMachine": "Publish state machine to Step Functions", @@ -247,7 +250,7 @@ "AWS.command.ssmDocument.openLocalDocumentJson": "Download as JSON", "AWS.command.ssmDocument.openLocalDocumentYaml": "Download as YAML", "AWS.command.ssmDocument.publishDocument": "Publish a Systems Manager Document", - "AWS.command.launchConfigForm.title": "Local Invoke and Debug Configuration", + "AWS.command.launchConfigForm.title": "Invoke Locally", "AWS.command.addSamDebugConfig": "Add Local Invoke and Debug Configuration", "AWS.command.toggleSamCodeLenses": "Toggle SAM hints in source files", "AWS.command.apprunner.createService": "Create Service", @@ -296,6 +299,7 @@ "AWS.appcomposer.explorerTitle": "Infrastructure Composer", "AWS.cdk.explorerTitle": "CDK", "AWS.codecatalyst.explorerTitle": "CodeCatalyst", + "AWS.sagemakerunifiedstudio.explorerTitle": "SageMaker Unified Studio", "AWS.cwl.limit.desc": "Maximum amount of log entries pulled per request from CloudWatch Logs. For LiveTail, when the limit is reached, the oldest events will be removed to accomodate new events. (max 10000)", "AWS.samcli.deploy.bucket.recentlyUsed": "Buckets recently used for SAM deployments", "AWS.submenu.amazonqEditorContextSubmenu.title": "Amazon Q", @@ -473,7 +477,8 @@ "AWS.toolkit.lambda.walkthrough.title": "Get started building your application", "AWS.toolkit.lambda.walkthrough.description": "Your quick guide to build an application visually, iterate locally, and deploy to the cloud!", "AWS.toolkit.lambda.walkthrough.toolInstall.title": "Complete installation", - "AWS.toolkit.lambda.walkthrough.toolInstall.description": "The AWS Command Line Interface (AWS CLI) is an open source tool that enables you to interact with AWS services using commands in your command-line shell. It is required to create and interact with AWS resources. \n\n[Install AWS CLI](command:aws.toolkit.installAWSCLI)\n\n Use the Serverless Application Model (SAM) CLI to locally build, invoke, and deploy your functions. Version 1.98+ is required. \n\n[Install SAM CLI](command:aws.toolkit.installSAMCLI)\n\n Use Docker to locally emulate a Lambda environment. Docker is optional. However, if you want to invoke locally, Docker is required so Lambda can locally emulate the execution environment. \n\n[Install Docker (optional)](command:aws.toolkit.installDocker)", + "AWS.toolkit.lambda.walkthrough.toolInstall.description.windows": "Manage your AWS services and resources with the AWS Command Line Interface (AWS CLI). \n\n[Install AWS CLI](command:aws.toolkit.installAWSCLI)\n\nBuild locally, invoke, and deploy your functions with the Serverless Application Model (SAM) CLI. \n\n[Install SAM CLI](command:aws.toolkit.installSAMCLI)\n\nDocker is an optional, third party tool that assists with local AWS Lambda runtime emulation. Docker is required to invoke Lambda functions on your local machine. \n\n[Install Docker (optional)](command:aws.toolkit.installDocker)\n\nEmulate your AWS cloud services locally with LocalStack to streamline testing in VS Code and CI environments. [Learn more](https://docs.localstack.cloud/aws/). \n\n[Install LocalStack (optional)](command:aws.toolkit.installLocalStack)", + "AWS.toolkit.lambda.walkthrough.toolInstall.description": "Manage your AWS services and resources with the AWS Command Line Interface (AWS CLI). \n\n[Install AWS CLI](command:aws.toolkit.installAWSCLI)\n\nBuild locally, invoke, and deploy your functions with the Serverless Application Model (SAM) CLI. \n\n[Install SAM CLI](command:aws.toolkit.installSAMCLI)\n\nDocker is an optional, third party tool that assists with local AWS Lambda runtime emulation. Docker is required to invoke Lambda functions on your local machine. \n\n[Install Docker (optional)](command:aws.toolkit.installDocker)\n\nEmulate your AWS cloud services locally with LocalStack to streamline testing in VS Code and CI environments. [Learn more]((https://docs.localstack.cloud/aws/). \n\n[Install LocalStack (optional)](command:aws.toolkit.installLocalStack)\n\nFinch is an open source tool for local container development. Finch aims to help promote innovative upstream container projects by making it easy to install and use them. [Learn more](https://runfinch.com/) \n\n[Install Finch (optional)](command:aws.toolkit.installFinch)", "AWS.toolkit.lambda.walkthrough.chooseTemplate.title": "Choose your application template", "AWS.toolkit.lambda.walkthrough.chooseTemplate.description": "Select a starter application, visually compose an application from scratch, open an existing application, or browse more application examples. \n\nInfrastructure Composer allows you to visually compose modern applications in the cloud. It will define the necessary permissions between resources when you drag a connection between them. \n\n[Initialize your project](command:aws.toolkit.lambda.initializeWalkthroughProject)", "AWS.toolkit.lambda.walkthrough.step1.title": "Iterate locally", diff --git a/packages/core/resources/amazonQCT/QCT-Maven-6-16.jar b/packages/core/resources/amazonQCT/QCT-Maven-1-0-156-0.jar similarity index 85% rename from packages/core/resources/amazonQCT/QCT-Maven-6-16.jar rename to packages/core/resources/amazonQCT/QCT-Maven-1-0-156-0.jar index bdc734b4d7b..8530e54fd5d 100644 Binary files a/packages/core/resources/amazonQCT/QCT-Maven-6-16.jar and b/packages/core/resources/amazonQCT/QCT-Maven-1-0-156-0.jar differ diff --git a/packages/core/resources/hyperpod_connect b/packages/core/resources/hyperpod_connect new file mode 100644 index 00000000000..3360f24bacb --- /dev/null +++ b/packages/core/resources/hyperpod_connect @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +# HyperPod Connection Script +# +# This script establishes a connection to an AWS SageMaker HyperPod instance using AWS Systems Manager (SSM). +# HyperPod is AWS's managed service for distributed machine learning training at scale. +# +# OVERVIEW: +# The script acts as a wrapper around the AWS SSM CLI to create a secure session tunnel to a HyperPod +# compute instance. It validates required parameters, logs the connection attempt, and executes the +# SSM StartSession command with HyperPod-specific connection details. +# +# REQUIRED ENVIRONMENT VARIABLES: +# AWS_REGION - AWS region where the HyperPod cluster is located (e.g., us-west-2) +# AWS_SSM_CLI - Path to the AWS SSM CLI executable +# STREAM_URL - WebSocket stream URL for the HyperPod session connection +# TOKEN - Authentication token for the HyperPod session +# SESSION_ID - Unique identifier for the HyperPod session +# +# OPTIONAL ENVIRONMENT VARIABLES: +# LOG_FILE_LOCATION - Path to log file (default: /tmp/hyperpod_connect.log) +# DEBUG_LOG - Enable debug logging (default: 0) +# +# USAGE: +# AWS_REGION=us-west-2 AWS_SSM_CLI=/usr/local/bin/session-manager-plugin \ +# STREAM_URL=wss://... TOKEN=abc123... SESSION_ID=session-xyz \ +# ./hyperpod_connect +# +# SECURITY NOTE: +# This script handles sensitive authentication tokens. Ensure proper file permissions +# and avoid logging sensitive values in production environments. +set -x +set -e +set -u + +_DATE_CMD=true + +if command > /dev/null 2>&1 -v date; then + _DATE_CMD=date +elif command > /dev/null 2>&1 -v /bin/date; then + _DATE_CMD=/bin/date +fi + +_log() { + echo "$("$_DATE_CMD" '+%Y/%m/%d %H:%M:%S')" "$@" >> "${LOG_FILE_LOCATION}" 2>&1 +} + +_require_nolog() { + if [ -z "${1:-}" ] || [ -z "${2:-}" ]; then + _log "error: missing required arg: $1" + exit 1 + fi +} + +_require() { + _require_nolog "$@" + _log "$1=$2" +} + +_hyperpod() { + # Function inputs + local AWS_SSM_CLI=$1 + local AWS_REGION=$2 + local STREAM_URL=$3 + local TOKEN=$4 + local SESSION_ID=$5 + + exec "$AWS_SSM_CLI" "{\"streamUrl\":\"$STREAM_URL\",\"tokenValue\":\"$TOKEN\",\"sessionId\":\"$SESSION_ID\"}" "$AWS_REGION" "StartSession" +} + +_main() { + # Set defaults for missing environment variables + DEBUG_LOG=${DEBUG_LOG:-0} + LOG_FILE_LOCATION=${LOG_FILE_LOCATION:-/tmp/hyperpod_connect.log} + + _log "==============================================================================" + _require AWS_REGION "${AWS_REGION:-}" + _require AWS_SSM_CLI "${AWS_SSM_CLI:-}" + _require SESSION_ID "${SESSION_ID:-}" + _require_nolog STREAM_URL "${STREAM_URL:-}" + _require_nolog TOKEN "${TOKEN:-}" + + _hyperpod "$AWS_SSM_CLI" "$AWS_REGION" "$STREAM_URL" "$TOKEN" "$SESSION_ID" +} + +_main \ No newline at end of file diff --git a/packages/core/resources/icons/aws/lambda/deployed-function.svg b/packages/core/resources/icons/aws/lambda/deployed-function.svg new file mode 100644 index 00000000000..5d4e1c89298 --- /dev/null +++ b/packages/core/resources/icons/aws/lambda/deployed-function.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/core/resources/icons/aws/lambda/invoke-remotely.svg b/packages/core/resources/icons/aws/lambda/invoke-remotely.svg new file mode 100644 index 00000000000..b6071674e0c --- /dev/null +++ b/packages/core/resources/icons/aws/lambda/invoke-remotely.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core/resources/icons/aws/sagemakerunifiedstudio/catalog.svg b/packages/core/resources/icons/aws/sagemakerunifiedstudio/catalog.svg new file mode 100644 index 00000000000..4bd5988c386 --- /dev/null +++ b/packages/core/resources/icons/aws/sagemakerunifiedstudio/catalog.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces-dark.svg b/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces-dark.svg new file mode 100644 index 00000000000..3d3950ef9be --- /dev/null +++ b/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces-dark.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces.svg b/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces.svg new file mode 100644 index 00000000000..e559fa399c7 --- /dev/null +++ b/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/core/resources/icons/aws/sagemakerunifiedstudio/symbol-int.svg b/packages/core/resources/icons/aws/sagemakerunifiedstudio/symbol-int.svg new file mode 100644 index 00000000000..18aa022e10f --- /dev/null +++ b/packages/core/resources/icons/aws/sagemakerunifiedstudio/symbol-int.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/core/resources/icons/aws/sagemakerunifiedstudio/table.svg b/packages/core/resources/icons/aws/sagemakerunifiedstudio/table.svg new file mode 100644 index 00000000000..a8ac2aac05d --- /dev/null +++ b/packages/core/resources/icons/aws/sagemakerunifiedstudio/table.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/resources/markdown/samReadme.md b/packages/core/resources/markdown/samReadme.md index 14022174844..8b6a08eed57 100644 --- a/packages/core/resources/markdown/samReadme.md +++ b/packages/core/resources/markdown/samReadme.md @@ -13,7 +13,7 @@ ${LISTOFCONFIGURATIONS} You can debug the Lambda handlers locally by adding a breakpoint to the source file, then running the launch configuration. This works by using Docker on your local machine. -Invocation parameters, including payloads and request parameters, can be edited either by the `Local Invoke and Debug Configuration` command (through the ${COMMANDPALETTE} or ${CODELENS}) or by editing the `launch.json` file. +Invocation parameters, including payloads and request parameters, can be edited either by the `Invoke Locally` command (through the ${COMMANDPALETTE} or ${CODELENS}) or by editing the `launch.json` file. ${COMPANYNAME} Lambda functions not defined in the [`template.yaml`](./template.yaml) file can be invoked and debugged by creating a launch configuration through the ${CODELENS} over the function declaration, or with the `Add Local Invoke and Debug Configuration` command. diff --git a/packages/core/resources/sagemaker_connect b/packages/core/resources/sagemaker_connect index 19d0e1984cc..ede46c1c4b3 100755 --- a/packages/core/resources/sagemaker_connect +++ b/packages/core/resources/sagemaker_connect @@ -46,7 +46,7 @@ _get_ssm_session_info_async() { # Generate unique temporary file name to avoid conflicts local temp_file="/tmp/ssm_session_response_$$_$(date +%s%N).json" - local max_retries=60 + local max_retries=8 local retry_interval=5 local attempt=1 diff --git a/packages/core/resources/sagemaker_connect.ps1 b/packages/core/resources/sagemaker_connect.ps1 index 034f9f09754..0e593d65b85 100644 --- a/packages/core/resources/sagemaker_connect.ps1 +++ b/packages/core/resources/sagemaker_connect.ps1 @@ -54,7 +54,7 @@ function Get-SSMSessionInfoAsync { $url = "http://localhost:$LocalEndpointPort/get_session_async?connection_identifier=$AwsResourceArn&credentials_type=$CredentialsType&request_id=$requestId" Write-Host "Calling Get-SSMSessionInfoAsync with URL: $url" - $maxRetries = 60 + $maxRetries = 8 $retryInterval = 5 for ($attempt = 1; $attempt -le $maxRetries; $attempt++) { diff --git a/packages/core/scripts/build/generateServiceClient.ts b/packages/core/scripts/build/generateServiceClient.ts index 5d1854527b9..6e6829fd1ec 100644 --- a/packages/core/scripts/build/generateServiceClient.ts +++ b/packages/core/scripts/build/generateServiceClient.ts @@ -241,6 +241,14 @@ void (async () => { serviceJsonPath: 'src/codewhisperer/client/user-service-2.json', serviceName: 'CodeWhispererUserClient', }, + { + serviceJsonPath: 'src/sagemakerunifiedstudio/shared/client/sqlworkbench.json', + serviceName: 'SQLWorkbench', + }, + { + serviceJsonPath: 'src/sagemakerunifiedstudio/shared/client/datazonecustomclient.json', + serviceName: 'DataZoneCustomClient', + }, ] await generateServiceClients(serviceClientDefinitions) })() diff --git a/packages/core/scripts/test/launchTestUtilities.ts b/packages/core/scripts/test/launchTestUtilities.ts index 92afb769275..6f8b420fd8a 100644 --- a/packages/core/scripts/test/launchTestUtilities.ts +++ b/packages/core/scripts/test/launchTestUtilities.ts @@ -121,6 +121,7 @@ async function getVSCodeCliArgs(params: { ['DEVELOPMENT_PATH']: projectRootDir, ['AWS_TOOLKIT_AUTOMATION']: params.suite, ['TEST_DIR']: process.env.TEST_DIR, + ['JAVA_HOME']: process.env.JAVA_HOME, ...params.env, }, } diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index e06b8ad53d9..aa266ce39dd 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -13,7 +13,6 @@ export { focusAmazonQChatWalkthrough, openAmazonQWalkthrough, walkthroughInlineSuggestionsExample, - walkthroughSecurityScanExample, } from './onboardingPage/walkthrough' export { api } from './extApi' export { AmazonQChatViewProvider } from './webview/webView' diff --git a/packages/core/src/amazonq/onboardingPage/walkthrough.ts b/packages/core/src/amazonq/onboardingPage/walkthrough.ts index cb56c8b2abb..50b1db642a5 100644 --- a/packages/core/src/amazonq/onboardingPage/walkthrough.ts +++ b/packages/core/src/amazonq/onboardingPage/walkthrough.ts @@ -7,7 +7,6 @@ import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerComm import globals, { isWeb } from '../../shared/extensionGlobals' import { VSCODE_EXTENSION_ID } from '../../shared/extensions' import { getLogger } from '../../shared/logger/logger' -import { localize } from '../../shared/utilities/vsCodeUtils' import { Commands, placeholder } from '../../shared/vscode/commands2' import vscode from 'vscode' @@ -66,11 +65,3 @@ fake_users = [ }) } ) - -export const walkthroughSecurityScanExample = Commands.declare( - `_aws.amazonq.walkthrough.securityScanExample`, - () => async () => { - const filterText = localize('AWS.command.amazonq.security.scan', 'Run Project Review') - void vscode.commands.executeCommand('workbench.action.quickOpen', `> ${filterText}`) - } -) diff --git a/packages/core/src/amazonq/webview/ui/quickActions/generator.ts b/packages/core/src/amazonq/webview/ui/quickActions/generator.ts index 4ca8b4cc10e..4a0c5bad16c 100644 --- a/packages/core/src/amazonq/webview/ui/quickActions/generator.ts +++ b/packages/core/src/amazonq/webview/ui/quickActions/generator.ts @@ -15,58 +15,17 @@ export interface QuickActionGeneratorProps { export class QuickActionGenerator { private isGumbyEnabled: boolean - private isScanEnabled: boolean private disabledCommands: string[] constructor(props: QuickActionGeneratorProps) { this.isGumbyEnabled = props.isGumbyEnabled - this.isScanEnabled = props.isScanEnabled this.disabledCommands = props.disableCommands ?? [] } public generateForTab(tabType: TabType): QuickActionCommandGroup[] { - // TODO: Update acc to UX const quickActionCommands = [ { commands: [ - ...(!this.disabledCommands.includes('/dev') - ? [ - { - command: '/dev', - icon: MynahIcons.CODE_BLOCK, - placeholder: 'Describe your task or issue in as much detail as possible', - description: 'Generate code to make a change in your project', - }, - ] - : []), - ...(!this.disabledCommands.includes('/test') - ? [ - { - command: '/test', - icon: MynahIcons.CHECK_LIST, - placeholder: 'Specify a function(s) in the current file (optional)', - description: 'Generate unit tests for selected code', - }, - ] - : []), - ...(this.isScanEnabled && !this.disabledCommands.includes('/review') - ? [ - { - command: '/review', - icon: MynahIcons.BUG, - description: 'Identify and fix code issues before committing', - }, - ] - : []), - ...(!this.disabledCommands.includes('/doc') - ? [ - { - command: '/doc', - icon: MynahIcons.FILE, - description: 'Generate documentation', - }, - ] - : []), ...(this.isGumbyEnabled && !this.disabledCommands.includes('/transform') ? [ { diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index a3d047bbbdc..5d21c6b77f7 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -403,9 +403,11 @@ export class GumbyController { await vscode.commands.executeCommand('aws.amazonq.transformationHub.summary.reveal') break case ButtonActions.STOP_TRANSFORMATION_JOB: - await stopTransformByQ(transformByQState.getJobId()) - await postTransformationJob() - await cleanupTransformationJob() + if (transformByQState.isRunning() || transformByQState.isRefreshInProgress()) { + await stopTransformByQ(transformByQState.getJobId()) + await postTransformationJob() + await cleanupTransformationJob() + } break case ButtonActions.CONFIRM_START_TRANSFORMATION_FLOW: this.resetTransformationChatFlow() @@ -580,11 +582,11 @@ export class GumbyController { return } const fileContents = await fs.readFileText(fileUri[0].fsPath) - const missingKey = await validateCustomVersionsFile(fileContents) + const errorMessage = validateCustomVersionsFile(fileContents) - if (missingKey) { + if (errorMessage) { this.messenger.sendMessage( - CodeWhispererConstants.invalidCustomVersionsFileMessage(missingKey), + CodeWhispererConstants.invalidCustomVersionsFileMessage(errorMessage), message.tabID, 'ai-prompt' ) diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts index 409ee89ab04..fb8da4c7096 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts @@ -400,11 +400,22 @@ export class Messenger { } public sendJobRefreshInProgressMessage(tabID: string, jobId: string) { - this.dispatcher.sendAsyncEventProgress( - new AsyncEventProgressMessage(tabID, { - inProgress: true, - message: CodeWhispererConstants.refreshingJobChatMessage(jobId), - }) + const buttons: ChatItemButton[] = [] + buttons.push({ + keepCardAfterClick: true, + text: CodeWhispererConstants.stopTransformationButtonText, + id: ButtonActions.STOP_TRANSFORMATION_JOB, + disabled: false, + }) + this.dispatcher.sendChatMessage( + new ChatMessage( + { + message: CodeWhispererConstants.refreshingJobChatMessage(jobId), + messageType: 'ai-prompt', + buttons: buttons, + }, + tabID + ) ) } diff --git a/packages/core/src/auth/auth.ts b/packages/core/src/auth/auth.ts index 6962b85bfa9..18f4fdd5b14 100644 --- a/packages/core/src/auth/auth.ts +++ b/packages/core/src/auth/auth.ts @@ -215,10 +215,34 @@ export class Auth implements AuthService, ConnectionManager { const provider = await this.getCredentialsProvider(id, profile) await this.authenticate(id, () => this.createCachedCredentials(provider), shouldInvalidate) - return this.getIamConnection(id, profile) + return await this.getIamConnection(id, profile) } } + /** + * Gets the SSO access token for a connection + * @param connection The SSO connection to get the token for + * @returns Promise resolving to the access token string + */ + @withTelemetryContext({ name: 'getSsoAccessToken', class: authClassName }) + public async getSsoAccessToken(connection: Pick): Promise { + const profile = this.store.getProfileOrThrow(connection.id) + + if (profile.type !== 'sso') { + throw new Error(`Connection ${connection.id} is not an SSO connection`) + } + + const provider = this.getSsoTokenProvider(connection.id, profile) + // Calling existing getToken private method - It will handle setting the connection state etc. + const token = await this._getToken(connection.id, provider) + + if (!token?.accessToken) { + throw new Error(`No access token available for connection ${connection.id}`) + } + + return token.accessToken + } + public async useConnection({ id }: Pick): Promise public async useConnection({ id }: Pick): Promise @withTelemetryContext({ name: 'useConnection', class: authClassName }) @@ -229,7 +253,8 @@ export class Auth implements AuthService, ConnectionManager { if (profile === undefined) { throw new Error(`Connection does not exist: ${id}`) } - const conn = profile.type === 'sso' ? this.getSsoConnection(id, profile) : this.getIamConnection(id, profile) + const conn = + profile.type === 'sso' ? this.getSsoConnection(id, profile) : await this.getIamConnection(id, profile) this.#activeConnection = conn this.#onDidChangeActiveConnection.fire(conn) @@ -573,7 +598,7 @@ export class Auth implements AuthService, ConnectionManager { } @withTelemetryContext({ name: 'updateConnectionState', class: authClassName }) - private async updateConnectionState(id: Connection['id'], connectionState: ProfileMetadata['connectionState']) { + public async updateConnectionState(id: Connection['id'], connectionState: ProfileMetadata['connectionState']) { getLogger().info(`auth: Updating connection state of ${id} to ${connectionState}`) if (connectionState === 'authenticating') { @@ -681,7 +706,7 @@ export class Auth implements AuthService, ConnectionManager { if (profile.type === 'sso') { return this.getSsoConnection(id, profile) } else { - return this.getIamConnection(id, profile) + return await this.getIamConnection(id, profile) } } @@ -781,10 +806,13 @@ export class Auth implements AuthService, ConnectionManager { ) } - private getIamConnection( + private async getIamConnection( id: Connection['id'], profile: StoredProfile - ): IamConnection & StatefulConnection { + ): Promise { + // Get the provider to extract the endpoint URL + const provider = await this.getCredentialsProvider(id, profile) + const endpointUrl = provider.getEndpointUrl?.() return { id, type: 'iam', @@ -792,6 +820,7 @@ export class Auth implements AuthService, ConnectionManager { label: profile.metadata.label ?? (profile.type === 'iam' && profile.subtype === 'linked' ? profile.name : id), getCredentials: async () => this.getCredentials(id, await this.getCredentialsProvider(id, profile)), + endpointUrl, } } @@ -808,6 +837,8 @@ export class Auth implements AuthService, ConnectionManager { label: profile.metadata?.label ?? this.getSsoProfileLabel(profile), getToken: () => this.getToken(id, provider), getRegistration: () => provider.getClientRegistration(), + // SsoConnection is managed internally in the AWS Toolkit, so the endpointUrl can't be configured + endpointUrl: undefined, } } @@ -831,9 +862,10 @@ export class Auth implements AuthService, ConnectionManager { private async createCachedCredentials(provider: CredentialsProvider) { const providerId = provider.getCredentialsId() + getLogger().debug(`credentials: create cache credentials for ${provider.getProviderType()}`) globals.loginManager.store.invalidateCredentials(providerId) - const { credentials } = await globals.loginManager.store.upsertCredentials(providerId, provider) - await globals.loginManager.validateCredentials(credentials, provider.getDefaultRegion()) + const { credentials, endpointUrl } = await globals.loginManager.store.upsertCredentials(providerId, provider) + await globals.loginManager.validateCredentials(credentials, endpointUrl, provider.getDefaultRegion()) return credentials } @@ -923,10 +955,22 @@ export class Auth implements AuthService, ConnectionManager { if (previousState === 'valid') { // Non-token expiration errors can happen. We must log it here, otherwise they are lost. getLogger().warn(`auth: valid connection became invalid. Last error: %s`, this.#validationErrors.get(id)) - const timeout = new Timeout(60000) this.#invalidCredentialsTimeouts.set(id, timeout) + // Check if this is a SMUS profile - if so, skip the generic prompt + // as SMUS has its own reauthentication flow + const isSmusConnection = profile.type === 'sso' && 'domainUrl' in profile && 'domainId' in profile + if (isSmusConnection) { + getLogger().debug(`auth: Skipping generic reauthentication prompt for SMUS connection ${id}`) + // For SMUS connections, just throw the InvalidConnection error + // The SMUS auth provider will handle showing the appropriate prompt + throw new ToolkitError('Connection is invalid or expired. Try logging in again.', { + code: errorCode.invalidConnection, + cause: this.#validationErrors.get(id), + }) + } + const connLabel = profile.metadata.label ?? (profile.type === 'sso' ? this.getSsoProfileLabel(profile) : id) const message = localize( 'aws.auth.invalidConnection', diff --git a/packages/core/src/auth/connection.ts b/packages/core/src/auth/connection.ts index 3e7752dd8e9..fea929fc8af 100644 --- a/packages/core/src/auth/connection.ts +++ b/packages/core/src/auth/connection.ts @@ -71,6 +71,18 @@ export const isBuilderIdConnection = (conn?: Connection): conn is SsoConnection export const isValidCodeCatalystConnection = (conn?: Connection): conn is SsoConnection => isSsoConnection(conn) && hasScopes(conn, scopesCodeCatalyst) +export const areCredentialsEqual = (creds1: any, creds2: any): boolean => { + if (!creds1 || !creds2) { + return creds1 === creds2 + } + + return ( + creds1.accessKeyId === creds2.accessKeyId && + creds1.secretAccessKey === creds2.secretAccessKey && + creds1.sessionToken === creds2.sessionToken + ) +} + export function hasScopes(target: SsoConnection | SsoProfile | string[], scopes: string[]): boolean { return scopes?.every((s) => (Array.isArray(target) ? target : target.scopes)?.includes(s)) } @@ -111,6 +123,7 @@ export function createSsoProfile( export interface SsoConnection extends SsoProfile { readonly id: string readonly label: string + readonly endpointUrl?: string | undefined /** * Retrieves a bearer token, refreshing or re-authenticating as-needed. @@ -129,6 +142,7 @@ export interface IamConnection { // This may change in the future after refactoring legacy implementations readonly id: string readonly label: string + readonly endpointUrl: string | undefined getCredentials(): Promise } diff --git a/packages/core/src/auth/credentials/store.ts b/packages/core/src/auth/credentials/store.ts index 53cc5573858..ff963b09db0 100644 --- a/packages/core/src/auth/credentials/store.ts +++ b/packages/core/src/auth/credentials/store.ts @@ -12,6 +12,7 @@ import { CredentialsProviderManager } from '../providers/credentialsProviderMana export interface CachedCredentials { credentials: AWS.Credentials credentialsHashCode: string + endpointUrl?: string } /** @@ -30,11 +31,16 @@ export class CredentialsStore { * If the expiration property does not exist, it is assumed to never expire. */ public isValid(key: string): boolean { + // Apply 60-second buffer similar to SSO token expiry logic + const expirationBufferMs = 60000 + if (this.credentialsCache[key]) { const expiration = this.credentialsCache[key].credentials.expiration - return expiration !== undefined ? expiration >= new globals.clock.Date() : true + const now = new globals.clock.Date() + const bufferedNow = new globals.clock.Date(now.getTime() + expirationBufferMs) + return expiration !== undefined ? expiration >= bufferedNow : true } - + getLogger().debug(`credentials: no credentials found for ${key}`) return false } @@ -89,13 +95,14 @@ export class CredentialsStore { credentialsId: CredentialsId, credentialsProvider: CredentialsProvider ): Promise { + getLogger().debug(`store: Fetch new credentials from provider with id: ${asString(credentialsId)}`) const credentials = { credentials: await credentialsProvider.getCredentials(), credentialsHashCode: credentialsProvider.getHashCode(), + endpointUrl: credentialsProvider.getEndpointUrl?.(), } this.credentialsCache[asString(credentialsId)] = credentials - return credentials } } diff --git a/packages/core/src/auth/credentials/types.ts b/packages/core/src/auth/credentials/types.ts index 79f3e623fcf..75a12a2d9b2 100644 --- a/packages/core/src/auth/credentials/types.ts +++ b/packages/core/src/auth/credentials/types.ts @@ -12,6 +12,7 @@ export const SharedCredentialsKeys = { AWS_SESSION_TOKEN: 'aws_session_token', CREDENTIAL_PROCESS: 'credential_process', CREDENTIAL_SOURCE: 'credential_source', + ENDPOINT_URL: 'endpoint_url', REGION: 'region', ROLE_ARN: 'role_arn', SOURCE_PROFILE: 'source_profile', diff --git a/packages/core/src/auth/credentials/utils.ts b/packages/core/src/auth/credentials/utils.ts index 885a4fb1f87..05a648d867d 100644 --- a/packages/core/src/auth/credentials/utils.ts +++ b/packages/core/src/auth/credentials/utils.ts @@ -21,7 +21,7 @@ import { isValidResponse } from '../../shared/wizards/wizard' const credentialsTimeout = 300000 // 5 minutes const credentialsProgressDelay = 1000 -export function asEnvironmentVariables(credentials: Credentials): NodeJS.ProcessEnv { +export function asEnvironmentVariables(credentials: Credentials, endpointUrl?: string): NodeJS.ProcessEnv { const environmentVariables: NodeJS.ProcessEnv = {} environmentVariables.AWS_ACCESS_KEY = credentials.accessKeyId @@ -30,6 +30,9 @@ export function asEnvironmentVariables(credentials: Credentials): NodeJS.Process environmentVariables.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey environmentVariables.AWS_SESSION_TOKEN = credentials.sessionToken environmentVariables.AWS_SECURITY_TOKEN = credentials.sessionToken + if (endpointUrl !== undefined) { + environmentVariables.AWS_ENDPOINT_URL = endpointUrl + } return environmentVariables } diff --git a/packages/core/src/auth/deprecated/loginManager.ts b/packages/core/src/auth/deprecated/loginManager.ts index b7c5a83d340..3a1b56d6079 100644 --- a/packages/core/src/auth/deprecated/loginManager.ts +++ b/packages/core/src/auth/deprecated/loginManager.ts @@ -30,10 +30,15 @@ import { isAutomation } from '../../shared/vscode/env' import { Credentials } from '@aws-sdk/types' import { ToolkitError } from '../../shared/errors' import * as localizedText from '../../shared/localizedText' -import { DefaultStsClient } from '../../shared/clients/stsClient' +import { + DefaultStsClient, + type GetCallerIdentityResponse, + type GetCallerIdentityResponseWithHeaders, +} from '../../shared/clients/stsClient' import { findAsync } from '../../shared/utilities/collectionUtils' import { telemetry } from '../../shared/telemetry/telemetry' import { withTelemetryContext } from '../../shared/telemetry/util' +import { localStackConnectionHeader, localStackConnectionString } from '../utils' const loginManagerClassName = 'LoginManager' /** @@ -65,19 +70,19 @@ export class LoginManager { try { provider = await getProvider(args.providerId) - - const credentials = (await this.store.upsertCredentials(args.providerId, provider))?.credentials + const { credentials, endpointUrl } = await this.store.upsertCredentials(args.providerId, provider) if (!credentials) { throw new Error(`No credentials found for id ${asString(args.providerId)}`) } - const accountId = await this.validateCredentials(credentials, provider.getDefaultRegion()) + const accountId = await this.validateCredentials(credentials, endpointUrl, provider.getDefaultRegion()) this.awsContext.credentialsShim = createCredentialsShim(this.store, args.providerId, credentials) await this.awsContext.setCredentials({ credentials, accountId: accountId, credentialsId: asString(args.providerId), defaultRegion: provider.getDefaultRegion(), + endpointUrl: provider.getEndpointUrl?.(), }) telemetryResult = 'Succeeded' @@ -111,16 +116,42 @@ export class LoginManager { } } - public async validateCredentials(credentials: Credentials, region = this.defaultCredentialsRegion) { - const stsClient = new DefaultStsClient(region, credentials) - const accountId = (await stsClient.getCallerIdentity()).Account + public async validateCredentials( + credentials: Credentials, + endpointUrl?: string, + region = this.defaultCredentialsRegion + ) { + const stsClient = new DefaultStsClient(region, credentials, endpointUrl) + const callerIdentity = await stsClient.getCallerIdentity() + await this.detectExternalConnection(callerIdentity) + // Validate presence of Account Id + const accountId = callerIdentity.Account if (!accountId) { + if (endpointUrl !== undefined) { + telemetry.auth_customEndpoint.emit({ source: 'validateCredentials', result: 'Failed' }) + } throw new Error('Could not determine Account Id for credentials') } + if (endpointUrl !== undefined) { + telemetry.auth_customEndpoint.emit({ source: 'validateCredentials', result: 'Succeeded' }) + } return accountId } + private async detectExternalConnection( + callerIdentity: GetCallerIdentityResponse | GetCallerIdentityResponseWithHeaders + ): Promise { + // SDK v3: Headers are captured via middleware and attached as $httpHeaders + const headers = (callerIdentity as GetCallerIdentityResponseWithHeaders).$httpHeaders + if (headers !== undefined && localStackConnectionHeader in headers) { + await globals.globalState.update('aws.toolkit.externalConnection', localStackConnectionString) + telemetry.auth_localstackEndpoint.emit({ source: 'validateCredentials', result: 'Succeeded' }) + } else { + await globals.globalState.update('aws.toolkit.externalConnection', undefined) + } + } + /** * Removes Credentials from the Toolkit. Essentially the Toolkit becomes "logged out". * diff --git a/packages/core/src/auth/index.ts b/packages/core/src/auth/index.ts index c180d603c67..a5a3ca0edd9 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -19,6 +19,7 @@ export { getTelemetryMetadataForConn, isIamConnection, isSsoConnection, + areCredentialsEqual, } from './connection' export { Auth } from './auth' export { CredentialsStore } from './credentials/store' diff --git a/packages/core/src/auth/providers/credentials.ts b/packages/core/src/auth/providers/credentials.ts index 56f1e6a2a00..2c86ffee4df 100644 --- a/packages/core/src/auth/providers/credentials.ts +++ b/packages/core/src/auth/providers/credentials.ts @@ -112,6 +112,10 @@ export interface CredentialsProvider { */ getTelemetryType(): CredentialType getDefaultRegion(): string | undefined + /** + * Gets the endpoint URL configured for this profile, if any. + */ + getEndpointUrl?(): string | undefined getHashCode(): string getCredentials(): Promise /** diff --git a/packages/core/src/auth/providers/envVarsCredentialsProvider.ts b/packages/core/src/auth/providers/envVarsCredentialsProvider.ts index dd9a78a7fcb..14cac0907a0 100644 --- a/packages/core/src/auth/providers/envVarsCredentialsProvider.ts +++ b/packages/core/src/auth/providers/envVarsCredentialsProvider.ts @@ -61,4 +61,9 @@ export class EnvVarsCredentialsProvider implements CredentialsProvider { } return this.credentials } + + public getEndpointUrl(): string | undefined { + const env = process.env as EnvironmentVariables + return env.AWS_ENDPOINT_URL?.toString() + } } diff --git a/packages/core/src/auth/providers/sharedCredentialsProvider.ts b/packages/core/src/auth/providers/sharedCredentialsProvider.ts index 407db4a717e..02d8f9b40f8 100644 --- a/packages/core/src/auth/providers/sharedCredentialsProvider.ts +++ b/packages/core/src/auth/providers/sharedCredentialsProvider.ts @@ -105,6 +105,10 @@ export class SharedCredentialsProvider implements CredentialsProvider { return this.profile[SharedCredentialsKeys.REGION] } + public getEndpointUrl(): string | undefined { + return this.profile[SharedCredentialsKeys.ENDPOINT_URL]?.trim() + } + public async canAutoConnect(): Promise { if (isSsoProfile(this.profile)) { const tokenProvider = SsoAccessTokenProvider.create({ diff --git a/packages/core/src/auth/providers/ssoCredentialsProvider.ts b/packages/core/src/auth/providers/ssoCredentialsProvider.ts index f38dd0710a2..e04ce1a3c06 100644 --- a/packages/core/src/auth/providers/ssoCredentialsProvider.ts +++ b/packages/core/src/auth/providers/ssoCredentialsProvider.ts @@ -61,4 +61,9 @@ export class SsoCredentialsProvider implements CredentialsProvider { private async hasToken() { return (await this.tokenProvider.getToken()) !== undefined } + + // SsoCredentials are managed internally in the AWS Toolkit, so the endpointUrl can't be configured + public getEndpointUrl(): undefined { + return undefined + } } diff --git a/packages/core/src/auth/secondaryAuth.ts b/packages/core/src/auth/secondaryAuth.ts index 01ccf6b799a..f8ea5d9b44f 100644 --- a/packages/core/src/auth/secondaryAuth.ts +++ b/packages/core/src/auth/secondaryAuth.ts @@ -18,7 +18,7 @@ import { withTelemetryContext } from '../shared/telemetry/util' import { isNetworkError } from '../shared/errors' import globals from '../shared/extensionGlobals' -export type ToolId = 'codecatalyst' | 'codewhisperer' | 'testId' +export type ToolId = 'codecatalyst' | 'codewhisperer' | 'testId' | 'smus' let currentConn: Auth['activeConnection'] const auths = new Map() diff --git a/packages/core/src/auth/sso/clients.ts b/packages/core/src/auth/sso/clients.ts index e921cb7856e..14bc35c039e 100644 --- a/packages/core/src/auth/sso/clients.ts +++ b/packages/core/src/auth/sso/clients.ts @@ -30,13 +30,14 @@ import { getLogger } from '../../shared/logger/logger' import { SsoAccessTokenProvider } from './ssoAccessTokenProvider' import { AwsClientResponseError, isClientFault } from '../../shared/errors' import { DevSettings } from '../../shared/settings' -import { SdkError } from '@aws-sdk/types' import { HttpRequest, HttpResponse } from '@smithy/protocol-http' import { StandardRetryStrategy, defaultRetryDecider } from '@smithy/middleware-retry' import { AuthenticationFlow } from './model' import { toSnakeCase } from '../../shared/utilities/textUtilities' import { getUserAgent, withTelemetryContext } from '../../shared/telemetry/util' import { oneSecond } from '../../shared/datetime' +import { telemetry } from '../../shared/telemetry/telemetry' +import { getTelemetryReason, getTelemetryReasonDesc, getHttpStatusCode } from '../../shared/errors' export class OidcClient { public constructor( @@ -87,15 +88,40 @@ export class OidcClient { } public async createToken(request: CreateTokenRequest) { + const startTime = this.clock.Date.now() + const grantType = request.grantType + let response try { response = await this.client.createToken(request as CreateTokenRequest) } catch (err) { + const statusCode = getHttpStatusCode(err) + telemetry.auth_ssoTokenOperation.emit({ + result: 'Failed', + grantType: grantType ?? 'unknown', + duration: this.clock.Date.now() - startTime, + reason: getTelemetryReason(err), + reasonDesc: getTelemetryReasonDesc(err), + ...(statusCode !== undefined ? { httpStatusCode: String(statusCode) } : {}), + }) + + getLogger().error(`sso-oidc: createToken failed (grantType=${grantType}): ${err}`) + const newError = AwsClientResponseError.instanceIf(err) throw newError } assertHasProps(response, 'accessToken', 'expiresIn') + telemetry.auth_ssoTokenOperation.emit({ + result: 'Succeeded', + grantType: grantType ?? 'unknown', + duration: this.clock.Date.now() - startTime, + }) + + getLogger().debug( + `sso-oidc: createToken succeeded (grantType=${grantType}, requestId=${response.$metadata.requestId})` + ) + return { ...selectFrom(response, 'accessToken', 'refreshToken', 'tokenType'), requestId: response.$metadata.requestId, @@ -104,22 +130,12 @@ export class OidcClient { } public static create(region: string) { - const updatedRetryDecider = (err: SdkError) => { - if (defaultRetryDecider(err)) { - return true - } - - // As part of SIM IDE-10703, there was an assumption that retrying on InvalidGrantException - // may be useful. This may not be the case anymore and if more research is done, this may not be needed. - // TODO: setup some telemetry to see if there are any successes on a subsequent retry for this case. - return err.name === 'InvalidGrantException' - } const client = new SSOOIDC({ region, endpoint: DevSettings.instance.get('endpoints', {})['ssooidc'], retryStrategy: new StandardRetryStrategy( () => Promise.resolve(3), // Maximum number of retries - { retryDecider: updatedRetryDecider } + { retryDecider: defaultRetryDecider } ), customUserAgent: getUserAgent({ includePlatform: true, includeClientId: true }), requestHandler: { diff --git a/packages/core/src/auth/sso/ssoAccessTokenProvider.ts b/packages/core/src/auth/sso/ssoAccessTokenProvider.ts index e753fb2ef90..a75f5738f5f 100644 --- a/packages/core/src/auth/sso/ssoAccessTokenProvider.ts +++ b/packages/core/src/auth/sso/ssoAccessTokenProvider.ts @@ -59,6 +59,15 @@ export abstract class SsoAccessTokenProvider { private static logIfChanged = onceChanged((s: string) => getLogger().info(s)) private readonly className = 'SsoAccessTokenProvider' + /** + * Prevents concurrent token refresh operations. + * Maps tokenCacheKey to an in-flight refresh promise. + */ + private static refreshPromises = new Map< + string, + Promise<{ token: SsoToken; registration: ClientRegistration; region: string; startUrl: string }> + >() + public static set authSource(val: string) { SsoAccessTokenProvider._authSource = val } @@ -108,15 +117,43 @@ export abstract class SsoAccessTokenProvider { true ) ) + if (!data || !isExpired(data.token)) { + getLogger().debug('Auth: token is valid, returning cached token (key=%s)', this.tokenCacheKey) return data?.token } + getLogger().info( + `Auth: bearer token expired (expires at ${data.token.expiresAt}), attempting refresh the token for (key=${this.tokenCacheKey})` + ) + if (data.registration && !isExpired(data.registration) && hasProps(data.token, 'refreshToken')) { - const refreshed = await this.refreshToken(data.token, data.registration) + getLogger().debug(`Auth: refresh token available, calling refreshToken() (key=${this.tokenCacheKey})`) + // Check if a refresh is already in progress for this token + const existingRefresh = SsoAccessTokenProvider.refreshPromises.get(this.tokenCacheKey) + if (existingRefresh) { + getLogger().debug( + 'SsoAccessTokenProvider: Token refresh already in progress, waiting for existing refresh' + ) + const refreshed = await existingRefresh + return refreshed.token + } + + // Start a new refresh and store the promise + const refreshPromise = this.refreshToken(data.token, data.registration) + SsoAccessTokenProvider.refreshPromises.set(this.tokenCacheKey, refreshPromise) - return refreshed.token + try { + const refreshed = await refreshPromise + return refreshed.token + } finally { + // Clean up the promise from the map once complete (success or failure) + SsoAccessTokenProvider.refreshPromises.delete(this.tokenCacheKey) + } } else { + getLogger().warn( + `getToken: cannot refresh - registration expired or no refresh token available (key=${this.tokenCacheKey})` + ) await this.invalidate('allCacheExpired') } } @@ -172,10 +209,18 @@ export abstract class SsoAccessTokenProvider { try { const clientInfo = selectFrom(registration, 'clientId', 'clientSecret') + getLogger().debug(`Auth refreshToken: calling OIDC createToken API (key=${this.tokenCacheKey})`) const response = await this.oidc.createToken({ ...clientInfo, ...token, grantType: refreshGrantType }) + + getLogger().debug(`Auth refreshToken: got response, now saving to cache...`) + const refreshed = this.formatToken(response, registration) + getLogger().debug(`refreshToken: saving refreshed token to cache (key=${this.tokenCacheKey})`) await this.cache.token.save(this.tokenCacheKey, refreshed) + getLogger().info( + `Auth refreshToken: token refresh successful (key=${this.tokenCacheKey}, new expiry=${response.expiresAt})` + ) telemetry.aws_refreshCredentials.emit({ result: 'Succeeded', requestId: response.requestId, @@ -184,6 +229,10 @@ export abstract class SsoAccessTokenProvider { return refreshed } catch (err) { + getLogger().error( + `Auth refreshToken: token refresh failed (key=${this.tokenCacheKey}): ${getErrorMsg(err as unknown as Error)}` + ) + if (err instanceof DiskCacheError) { /** * Background: @@ -197,6 +246,9 @@ export abstract class SsoAccessTokenProvider { * to the logs where the error was logged. Hopefully they can use this information to fix the issue, * or at least hint for them to provide the logs in a bug report. */ + getLogger().warn( + `Auth refreshToken: DiskCacheError during refresh, not invalidating session (key=${this.tokenCacheKey})` + ) void DiskCacheErrorMessage.instance.showMessageThrottled(err) } else if (!isNetworkError(err)) { const reason = getTelemetryReason(err) diff --git a/packages/core/src/auth/ui/statusBarItem.ts b/packages/core/src/auth/ui/statusBarItem.ts index a70a905ed6d..e253a6f427e 100644 --- a/packages/core/src/auth/ui/statusBarItem.ts +++ b/packages/core/src/auth/ui/statusBarItem.ts @@ -51,12 +51,6 @@ function handleDevSettings(statusBarItem: vscode.StatusBarItem, devSettings: Dev function updateItem(statusBarItem: vscode.StatusBarItem, devSettings: DevSettings): void { const company = getIdeProperties().company const connections = getAllConnectionsInUse(Auth.instance) - const connectedTooltip = localize( - 'AWS.credentials.statusbar.connected', - 'Connected to {0} with "{1}" (click to change)', - getIdeProperties().company, - connections[0]?.label - ) const disconnectedTooltip = localize( 'AWS.credentials.statusbar.disconnected', 'Click to connect to {0}', @@ -69,7 +63,25 @@ function updateItem(statusBarItem: vscode.StatusBarItem, devSettings: DevSetting statusBarItem.text = company statusBarItem.tooltip = disconnectedTooltip } else if (connections.length === 1) { - statusBarItem.text = getText(connections[0].label) + // Get the endpoint URL if available + const endpointUrl = connections[0].endpointUrl + const connectedTooltip = endpointUrl + ? localize( + 'AWS.credentials.statusbar.connected.endpoint', + 'Connected to {0} with "{1}" ({2}) (click to change)', + getIdeProperties().company, + connections[0]?.label, + endpointUrl + ) + : localize( + 'AWS.credentials.statusbar.connected', + 'Connected to {0} with "{1}" (click to change)', + getIdeProperties().company, + connections[0]?.label + ) + + const displayText = endpointUrl ? `${connections[0].label} (custom endpoint)` : connections[0].label + statusBarItem.text = getText(displayText) statusBarItem.tooltip = connectedTooltip } else { const expired = connections.filter((c) => c.state !== 'valid') diff --git a/packages/core/src/auth/utils.ts b/packages/core/src/auth/utils.ts index b455780f45d..28e2bc1123e 100644 --- a/packages/core/src/auth/utils.ts +++ b/packages/core/src/auth/utils.ts @@ -487,10 +487,13 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' | const state = auth.getConnectionState(conn) // Only allow SSO connections to be deleted const deleteButton: vscode.QuickInputButton[] = conn.type === 'sso' ? [createDeleteConnectionButton()] : [] + // Get endpoint URL if available + const connLabel = conn.endpointUrl ? `${conn.label} (${conn.endpointUrl})` : conn.label if (state === 'valid') { + const label = codicon`${getConnectionIcon(conn)} ${connLabel}` return { data: conn, - label: codicon`${getConnectionIcon(conn)} ${conn.label}`, + label: label, description: await getConnectionDescription(conn), buttons: [...deleteButton], } @@ -509,7 +512,7 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' | detail: getDetail(), data: conn, invalidSelection: state !== 'authenticating', - label: codicon`${getIcon('vscode-error')} ${conn.label}`, + label: codicon`${getIcon('vscode-error')} ${connLabel}`, buttons: [...deleteButton], description: state === 'authenticating' @@ -607,7 +610,14 @@ export class AuthNode implements TreeNode { const conn = this.resource.activeConnection const itemLabel = conn?.label !== undefined - ? localize('aws.auth.node.connected', `Connected with {0}`, conn.label) + ? conn?.endpointUrl !== undefined + ? localize( + 'aws.auth.node.connectedWithEndpoint', + `Connected with {0} ({1})`, + conn.label, + conn?.endpointUrl + ) + : localize('aws.auth.node.connected', `Connected with {0}`, conn.label) : localize('aws.auth.node.selectConnection', 'Select a connection...') const item = new vscode.TreeItem(itemLabel) @@ -880,3 +890,12 @@ export async function getAuthType() { } return authType } + +export const localStackConnectionHeader = 'x-localstack' +export const localStackConnectionString = 'localstack' + +export function isLocalStackConnection(): boolean { + return ( + globals.globalState.tryGet('aws.toolkit.externalConnection', String, undefined) === localStackConnectionString + ) +} diff --git a/packages/core/src/awsService/accessanalyzer/vue/constants.ts b/packages/core/src/awsService/accessanalyzer/vue/constants.ts index d43c8ba23c4..2c0ddb0585b 100644 --- a/packages/core/src/awsService/accessanalyzer/vue/constants.ts +++ b/packages/core/src/awsService/accessanalyzer/vue/constants.ts @@ -30,8 +30,6 @@ export type PolicyChecksCheckType = 'CheckNoNewAccess' | 'CheckAccessNotGranted' export type PolicyChecksPolicyType = 'Identity' | 'Resource' -export type ValidatePolicyFindingType = 'ERROR' | 'SECURITY_WARNING' | 'SUGGESTION' | 'WARNING' - export type PolicyChecksResult = 'Success' | 'Warning' | 'Error' export type PolicyChecksUiClick = diff --git a/packages/core/src/awsService/accessanalyzer/vue/iamPolicyChecks.ts b/packages/core/src/awsService/accessanalyzer/vue/iamPolicyChecks.ts index e7603400c0a..42521e73ec6 100644 --- a/packages/core/src/awsService/accessanalyzer/vue/iamPolicyChecks.ts +++ b/packages/core/src/awsService/accessanalyzer/vue/iamPolicyChecks.ts @@ -11,7 +11,8 @@ import { localize } from '../../../shared/utilities/vsCodeUtils' import { VueWebview, VueWebviewPanel } from '../../../webviews/main' import { ExtContext } from '../../../shared/extensions' import { telemetry } from '../../../shared/telemetry/telemetry' -import { AccessAnalyzer, SharedIniFileCredentials } from 'aws-sdk' +import { AccessAnalyzerClient, ValidatePolicyCommand } from '@aws-sdk/client-accessanalyzer' +import { fromIni } from '@aws-sdk/credential-providers' import { ToolkitError } from '../../../shared/errors' import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../../shared/filesystemUtilities' import globals from '../../../shared/extensionGlobals' @@ -23,7 +24,6 @@ import { PolicyChecksPolicyType, PolicyChecksResult, PolicyChecksUiClick, - ValidatePolicyFindingType, } from './constants' import { S3Client, parseS3Uri } from '../../../shared/clients/s3' import { ExpiredTokenException } from '@aws-sdk/client-sso-oidc' @@ -61,7 +61,7 @@ export class IamPolicyChecksWebview extends VueWebview { public constructor( private readonly data: IamPolicyChecksInitialData, - private client: AccessAnalyzer, + private client: AccessAnalyzerClient, private readonly region: string, public readonly onChangeInputPath = new vscode.EventEmitter(), public readonly onChangeCheckNoNewAccessFilePath = new vscode.EventEmitter(), @@ -179,85 +179,94 @@ export class IamPolicyChecksWebview extends VueWebview { documentType, inputPolicyType: policyType ? policyType : 'None', }) - this.client.config.credentials = new SharedIniFileCredentials({ + this.client.config.credentials = fromIni({ profile: `${getProfileName()}`, }) // We need to detect changes in the user's credentials - this.client.validatePolicy( - { - policyDocument: IamPolicyChecksWebview.editedDocument, - policyType: policyType === 'Identity' ? 'IDENTITY_POLICY' : 'RESOURCE_POLICY', - }, - (err, data) => { - if (err) { + this.client + .send( + new ValidatePolicyCommand({ + policyDocument: IamPolicyChecksWebview.editedDocument, + policyType: policyType === 'Identity' ? 'IDENTITY_POLICY' : 'RESOURCE_POLICY', + }) + ) + .then((data) => { + if (data.findings && data.findings.length > 0) { span.record({ - findingsCount: 0, + findingsCount: data.findings.length, }) - if (err instanceof ExpiredTokenException) { - this.onValidatePolicyResponse.fire([ - IamPolicyChecksConstants.InvalidAwsCredentials, - getResultCssColor('Error'), - ]) - } else { - this.onValidatePolicyResponse.fire([err.message, getResultCssColor('Error')]) - } - } else { - if (data.findings.length > 0) { - span.record({ - findingsCount: data.findings.length, - }) - // eslint-disable-next-line unicorn/no-array-for-each - data.findings.forEach((finding: AccessAnalyzer.ValidatePolicyFinding) => { - const message = `${finding.findingType}: ${finding.issueCode} - ${finding.findingDetails} Learn more: ${finding.learnMoreLink}` - if ((finding.findingType as ValidatePolicyFindingType) === 'ERROR') { - diagnostics.push( - new vscode.Diagnostic( - new vscode.Range( - finding.locations[0].span.start.line, - finding.locations[0].span.start.offset, - finding.locations[0].span.end.line, - finding.locations[0].span.end.offset - ), - message, - vscode.DiagnosticSeverity.Error - ) - ) - validatePolicyDiagnosticCollection.set( - IamPolicyChecksWebview.editedDocumentUri, - diagnostics + // eslint-disable-next-line unicorn/no-array-for-each + data.findings.forEach((finding) => { + const locationSpan = finding.locations?.[0].span + if ( + !locationSpan?.start?.line || + !locationSpan.start.offset || + !locationSpan.end?.line || + !locationSpan.end.offset + ) { + return + } + const message = `${finding.findingType}: ${finding.issueCode} - ${finding.findingDetails} Learn more: ${finding.learnMoreLink}` + if (finding.findingType === 'ERROR') { + diagnostics.push( + new vscode.Diagnostic( + new vscode.Range( + locationSpan.start.line, + locationSpan.start.offset, + locationSpan.end.line, + locationSpan.end.offset + ), + message, + vscode.DiagnosticSeverity.Error ) - } else { - diagnostics.push( - new vscode.Diagnostic( - new vscode.Range( - finding.locations[0].span.start.line, - finding.locations[0].span.start.offset, - finding.locations[0].span.end.line, - finding.locations[0].span.end.offset - ), - message, - vscode.DiagnosticSeverity.Warning - ) + ) + validatePolicyDiagnosticCollection.set( + IamPolicyChecksWebview.editedDocumentUri, + diagnostics + ) + } else { + diagnostics.push( + new vscode.Diagnostic( + new vscode.Range( + locationSpan.start.line, + locationSpan.start.offset, + locationSpan.end.line, + locationSpan.end.offset + ), + message, + vscode.DiagnosticSeverity.Warning ) - validatePolicyDiagnosticCollection.set( - IamPolicyChecksWebview.editedDocumentUri, - diagnostics - ) - } - }) - this.onValidatePolicyResponse.fire([ - IamPolicyChecksConstants.ValidatePolicySuccessWithFindings, - getResultCssColor('Warning'), - ]) - void vscode.commands.executeCommand('workbench.actions.view.problems') - } else { - this.onValidatePolicyResponse.fire([ - IamPolicyChecksConstants.ValidatePolicySuccessNoFindings, - getResultCssColor('Success'), - ]) - } + ) + validatePolicyDiagnosticCollection.set( + IamPolicyChecksWebview.editedDocumentUri, + diagnostics + ) + } + }) + this.onValidatePolicyResponse.fire([ + IamPolicyChecksConstants.ValidatePolicySuccessWithFindings, + getResultCssColor('Warning'), + ]) + void vscode.commands.executeCommand('workbench.actions.view.problems') + } else { + this.onValidatePolicyResponse.fire([ + IamPolicyChecksConstants.ValidatePolicySuccessNoFindings, + getResultCssColor('Success'), + ]) + } + }) + .catch((err) => { + span.record({ + findingsCount: 0, + }) + if (err instanceof ExpiredTokenException) { + this.onValidatePolicyResponse.fire([ + IamPolicyChecksConstants.InvalidAwsCredentials, + getResultCssColor('Error'), + ]) + } else { + this.onValidatePolicyResponse.fire([err.message, getResultCssColor('Error')]) } - } - ) + }) }) return } else { @@ -781,7 +790,7 @@ const Panel = VueWebview.compilePanel(IamPolicyChecksWebview) export async function renderIamPolicyChecks(context: ExtContext): Promise { const logger: Logger = getLogger() try { - const client = new AccessAnalyzer({ region: context.regionProvider.defaultRegionId }) + const client = new AccessAnalyzerClient({ region: context.regionProvider.defaultRegionId }) // Read from settings to auto-fill some inputs const checkNoNewAccessFilePath: string = vscode.workspace .getConfiguration() diff --git a/packages/core/src/awsService/apigateway/commands/copyUrl.ts b/packages/core/src/awsService/apigateway/commands/copyUrl.ts index 583c0ead91d..7c19ffa9254 100644 --- a/packages/core/src/awsService/apigateway/commands/copyUrl.ts +++ b/packages/core/src/awsService/apigateway/commands/copyUrl.ts @@ -11,7 +11,7 @@ import * as picker from '../../../shared/ui/picker' import * as vscode from 'vscode' import { ProgressLocation } from 'vscode' -import { Stage } from 'aws-sdk/clients/apigateway' +import { Stage } from '@aws-sdk/client-api-gateway' import { ApiGatewayClient } from '../../../shared/clients/apiGateway' import { defaultDnsSuffix, RegionProvider } from '../../../shared/regions/regionProvider' import { getLogger } from '../../../shared/logger/logger' diff --git a/packages/core/src/awsService/apigateway/explorer/apiGatewayNodes.ts b/packages/core/src/awsService/apigateway/explorer/apiGatewayNodes.ts index 30a359d80ad..c23b6cb972f 100644 --- a/packages/core/src/awsService/apigateway/explorer/apiGatewayNodes.ts +++ b/packages/core/src/awsService/apigateway/explorer/apiGatewayNodes.ts @@ -12,7 +12,7 @@ import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' import { PlaceholderNode } from '../../../shared/treeview/nodes/placeholderNode' import { compareTreeItems, makeChildrenNodes } from '../../../shared/treeview/utils' import { ApiGatewayClient } from '../../../shared/clients/apiGateway' -import { RestApi } from 'aws-sdk/clients/apigateway' +import { RestApi } from '@aws-sdk/client-api-gateway' import { toArrayAsync, toMap, updateInPlace } from '../../../shared/utilities/collectionUtils' import { RestApiNode } from './apiNodes' diff --git a/packages/core/src/awsService/apigateway/explorer/apiNodes.ts b/packages/core/src/awsService/apigateway/explorer/apiNodes.ts index 5e20de603f0..02280a74368 100644 --- a/packages/core/src/awsService/apigateway/explorer/apiNodes.ts +++ b/packages/core/src/awsService/apigateway/explorer/apiNodes.ts @@ -5,7 +5,7 @@ import { AWSResourceNode } from '../../../shared/treeview/nodes/awsResourceNode' import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' -import { RestApi } from 'aws-sdk/clients/apigateway' +import { RestApi } from '@aws-sdk/client-api-gateway' export class RestApiNode extends AWSTreeNodeBase implements AWSResourceNode { public override id!: string diff --git a/packages/core/src/awsService/apigateway/vue/invokeRemoteRestApi.ts b/packages/core/src/awsService/apigateway/vue/invokeRemoteRestApi.ts index df4f441d5c0..4e8389c89bc 100644 --- a/packages/core/src/awsService/apigateway/vue/invokeRemoteRestApi.ts +++ b/packages/core/src/awsService/apigateway/vue/invokeRemoteRestApi.ts @@ -8,7 +8,7 @@ import { RestApiNode } from '../explorer/apiNodes' import { getLogger, Logger } from '../../../shared/logger/logger' import { toArrayAsync } from '../../../shared/utilities/collectionUtils' -import { Resource } from 'aws-sdk/clients/apigateway' +import { Resource } from '@aws-sdk/client-api-gateway' import { localize } from '../../../shared/utilities/vsCodeUtils' import { Result } from '../../../shared/telemetry/telemetry' import { VueWebview } from '../../../webviews/main' diff --git a/packages/core/src/awsService/appBuilder/activation.ts b/packages/core/src/awsService/appBuilder/activation.ts index c4718549230..93cf1119448 100644 --- a/packages/core/src/awsService/appBuilder/activation.ts +++ b/packages/core/src/awsService/appBuilder/activation.ts @@ -13,7 +13,12 @@ import { activateViewsShared, registerToolView } from '../../awsexplorer/activat import { setContext } from '../../shared/vscode/setContext' import { fs } from '../../shared/fs/fs' import { AppBuilderRootNode } from './explorer/nodes/rootNode' -import { initWalkthroughProjectCommand, walkthroughContextString, getOrInstallCliWrapper } from './walkthrough' +import { + initWalkthroughProjectCommand, + walkthroughContextString, + getOrInstallCliWrapper, + installLocalStackExtension, +} from './walkthrough' import { getLogger } from '../../shared/logger/logger' import path from 'path' import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider' @@ -142,6 +147,12 @@ async function registerAppBuilderCommands(context: ExtContext): Promise { Commands.register('aws.toolkit.installDocker', async () => { await getOrInstallCliWrapper('docker', source) }), + Commands.register('aws.toolkit.installLocalStack', async () => { + await installLocalStackExtension(source) + }), + Commands.register('aws.toolkit.installFinch', async () => { + await getOrInstallCliWrapper('finch', source) + }), Commands.register('aws.toolkit.lambda.setWalkthroughToAPI', async () => { await setWalkthrough('API') }), diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts index 100c6802c52..d135185e71d 100644 --- a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts @@ -13,7 +13,8 @@ import { getLogger } from '../../../../shared/logger/logger' import { DefaultLambdaClient } from '../../../../shared/clients/lambdaClient' import globals from '../../../../shared/extensionGlobals' import { defaultPartition } from '../../../../shared/regions/regionProvider' -import { Lambda, APIGateway } from 'aws-sdk' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' +import { RestApi } from '@aws-sdk/client-api-gateway' import { LambdaNode } from '../../../../lambda/explorer/lambdaNodes' import { LambdaFunctionNode } from '../../../../lambda/explorer/lambdaFunctionNode' import { S3Client, toBucket } from '../../../../shared/clients/s3' @@ -25,9 +26,11 @@ import { SERVERLESS_FUNCTION_TYPE, SERVERLESS_API_TYPE, s3BucketType, + SERVERLESS_CAPACITY_PROVIDER_TYPE, } from '../../../../shared/cloudformation/cloudformation' import { ToolkitError } from '../../../../shared/errors' -import { ResourceTreeEntity } from '../samProject' +import { ResourceTreeEntity, isFunctionResource } from '../samProject' +import { LambdaCapacityProviderNode } from '../../../../lambda/explorer/lambdaCapacityProviderNode' const localize = nls.loadMessageBundle() export interface DeployedResource { @@ -42,6 +45,7 @@ export const DeployedResourceContextValues: Record = { [SERVERLESS_FUNCTION_TYPE]: 'awsRegionFunctionNodeDownloadable', [SERVERLESS_API_TYPE]: 'awsApiGatewayNode', [s3BucketType]: 'awsS3BucketNode', + [SERVERLESS_CAPACITY_PROVIDER_TYPE]: 'awsCapacityProviderNode', } export class DeployedResourceNode implements TreeNode { @@ -80,7 +84,7 @@ export async function generateDeployedNode( stackName: string, resourceTreeEntity: ResourceTreeEntity, location?: vscode.Uri -): Promise { +): Promise { let newDeployedResource: any const partitionId = globals.regionProvider.getPartitionId(regionCode) ?? defaultPartition try { @@ -88,16 +92,19 @@ export async function generateDeployedNode( case SERVERLESS_FUNCTION_TYPE: { const defaultClient = new DefaultLambdaClient(regionCode) const lambdaNode = new LambdaNode(regionCode, defaultClient) - let configuration: Lambda.FunctionConfiguration + let configuration: FunctionConfiguration try { configuration = (await defaultClient.getFunction(deployedResource.PhysicalResourceId)) - .Configuration as Lambda.FunctionConfiguration + .Configuration as FunctionConfiguration + const codeUri = isFunctionResource(resourceTreeEntity) ? resourceTreeEntity.CodeUri : undefined newDeployedResource = new LambdaFunctionNode( lambdaNode, regionCode, configuration, undefined, - location ? vscode.Uri.joinPath(location, resourceTreeEntity.CodeUri ?? '').fsPath : undefined + location ? vscode.Uri.joinPath(location, codeUri ?? '').fsPath : undefined, + location, + deployedResource.LogicalResourceId ) } catch (error: any) { getLogger().error('Error getting Lambda configuration: %O', error) @@ -118,12 +125,11 @@ export async function generateDeployedNode( const apiParentNode = new ApiGatewayNode(partitionId, regionCode) const apiNodes = await apiParentNode.getChildren() const apiNode = apiNodes.find((node) => node.id === deployedResource.PhysicalResourceId) - newDeployedResource = new RestApiNode( - apiParentNode, - partitionId, - regionCode, - apiNode as APIGateway.RestApi - ) + newDeployedResource = new RestApiNode(apiParentNode, partitionId, regionCode, apiNode as RestApi) + break + } + case SERVERLESS_CAPACITY_PROVIDER_TYPE: { + newDeployedResource = new LambdaCapacityProviderNode(regionCode, deployedResource) break } default: diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts index 1bf8381e097..3a533c722fb 100644 --- a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts @@ -5,9 +5,10 @@ import * as vscode from 'vscode' import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider' import { getIcon } from '../../../../shared/icons' -import { CloudFormationClient, DescribeStacksCommand } from '@aws-sdk/client-cloudformation' +import { CloudFormationClient, DescribeStacksCommand, CloudFormationClientConfig } from '@aws-sdk/client-cloudformation' import { ToolkitError } from '../../../../shared/errors' import { getIAMConnection } from '../../../../auth/utils' +import globals from '../../../../shared/extensionGlobals' export class StackNameNode implements TreeNode { public readonly id = this.stackName @@ -46,7 +47,12 @@ export async function generateStackNode(stackName?: string, regionCode?: string) return [] } const cred = await connection.getCredentials() - const client = new CloudFormationClient({ region: regionCode, credentials: cred }) + const endpointUrl = globals.awsContext.getCredentialEndpointUrl() + const opts: CloudFormationClientConfig = { region: regionCode, credentials: cred } + if (endpointUrl !== undefined) { + opts.endpoint = endpointUrl + } + const client = new CloudFormationClient(opts) try { const command = new DescribeStacksCommand({ StackName: stackName }) const response = await client.send(command) diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/propertyNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/propertyNode.ts index 481ecdf7009..e6eb92ff22a 100644 --- a/packages/core/src/awsService/appBuilder/explorer/nodes/propertyNode.ts +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/propertyNode.ts @@ -7,6 +7,16 @@ import * as vscode from 'vscode' import { getIcon } from '../../../../shared/icons' import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider' +/** + * Formats CloudFormation intrinsic functions into readable strings + */ +function formatIntrinsicFunction(value: any): string | undefined { + if (typeof value !== 'object' || value === null || Object.keys(value).length !== 1) { + return undefined + } + return JSON.stringify(value) +} + export class PropertyNode implements TreeNode { public readonly id = this.key public readonly resource = this.value @@ -25,12 +35,15 @@ export class PropertyNode implements TreeNode { } public getTreeItem() { - const item = new vscode.TreeItem(`${this.key}: ${this.value}`) + const intrinsicFormat = formatIntrinsicFunction(this.value) + const displayValue = intrinsicFormat ?? this.value + + const item = new vscode.TreeItem(`${this.key}: ${displayValue}`) item.contextValue = 'awsAppBuilderPropertyNode' item.iconPath = getIcon('vscode-gear') - if (this.value instanceof Array || this.value instanceof Object) { + if (!intrinsicFormat && (this.value instanceof Array || this.value instanceof Object)) { item.label = this.key item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed } @@ -41,6 +54,6 @@ export class PropertyNode implements TreeNode { export function generatePropertyNodes(properties: { [key: string]: any }): TreeNode[] { return Object.entries(properties) - .filter(([key, _]) => key !== 'Id' && key !== 'Type' && key !== 'Events') + .filter(([key, value]) => key !== 'Id' && key !== 'Type' && key !== 'Events' && value !== undefined) .map(([key, value]) => new PropertyNode(key, value)) } diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts index 72de5afc60f..134302b0417 100644 --- a/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts @@ -12,18 +12,43 @@ import { s3BucketType, appRunnerType, ecrRepositoryType, + SERVERLESS_CAPACITY_PROVIDER_TYPE, } from '../../../../shared/cloudformation/cloudformation' import { generatePropertyNodes } from './propertyNode' import { generateDeployedNode } from './deployedNode' import { StackResource } from '../../../../lambda/commands/listSamResources' import { DeployedResourceNode } from './deployedNode' +import { LambdaFunctionNode } from '../../../../lambda/explorer/lambdaFunctionNode' +import { ToolkitError } from '../../../../shared/errors' enum ResourceTypeId { Function = 'function', + DeployedFunction = 'deployed-function', Api = 'api', + CapacityProvider = 'capacityprovider', Other = '', } +export async function generateLambdaNodeFromResource(resource: ResourceNode['resource']): Promise { + if (!resource.deployedResource || !resource.region || !resource.stackName || !resource.resource) { + throw new ToolkitError('Error getting Lambda info from Appbuilder Node, please check your connection') + } + const nodes = (await generateDeployedNode( + resource.deployedResource, + resource.region, + resource.stackName, + resource.resource, + resource.projectRoot + )) as DeployedResourceNode[] + if (nodes.length !== 1) { + throw new ToolkitError('Error getting Lambda info from Appbuilder Node, please check your connection') + } + // lambda function node or undefined + return nodes[0].resource?.explorerNode +} + +// from here, we should have a helper function to detect if lambda is deployed +// then return deployed node/normal node on each condition. export class ResourceNode implements TreeNode { public readonly id = this.resourceTreeEntity.Id private readonly type = this.resourceTreeEntity.Type @@ -43,6 +68,7 @@ export class ResourceNode implements TreeNode { return { resource: this.resourceTreeEntity, location: this.location.samTemplateUri, + projectRoot: this.location.projectRoot, workspaceFolder: this.location.workspaceFolder, region: this.region, stackName: this.stackName, @@ -56,15 +82,18 @@ export class ResourceNode implements TreeNode { let propertyNodes: TreeNode[] = [] if (this.deployedResource && this.region && this.stackName) { - deployedNodes = await generateDeployedNode( + deployedNodes = (await generateDeployedNode( this.deployedResource, this.region, this.stackName, this.resourceTreeEntity, this.location.projectRoot - ) + )) as DeployedResourceNode[] } - if (this.resourceTreeEntity.Type === SERVERLESS_FUNCTION_TYPE) { + if ( + this.resourceTreeEntity.Type === SERVERLESS_FUNCTION_TYPE || + this.resourceTreeEntity.Type === SERVERLESS_CAPACITY_PROVIDER_TYPE + ) { propertyNodes = generatePropertyNodes(this.resourceTreeEntity) } @@ -72,10 +101,7 @@ export class ResourceNode implements TreeNode { } public getTreeItem(): vscode.TreeItem { - // Determine the initial TreeItem collapsible state based on the type - const collapsibleState = this.deployedResource - ? vscode.TreeItemCollapsibleState.Collapsed - : vscode.TreeItemCollapsibleState.None + const collapsibleState = vscode.TreeItemCollapsibleState.Collapsed // Create the TreeItem with the determined collapsible state const item = new vscode.TreeItem(this.resourceTreeEntity.Id, collapsibleState) @@ -100,13 +126,19 @@ export class ResourceNode implements TreeNode { private getIconPath(): IconPath | undefined { switch (this.type) { case SERVERLESS_FUNCTION_TYPE: + if (this.deployedResource) { + return getIcon('aws-lambda-deployed-function') + } return getIcon('aws-lambda-function') + // add deployed lambda function type case s3BucketType: return getIcon('aws-s3-bucket') case appRunnerType: return getIcon('aws-apprunner-service') case ecrRepositoryType: return getIcon('aws-ecr-registry') + case SERVERLESS_CAPACITY_PROVIDER_TYPE: + return getIcon('vscode-gear') default: return getIcon('vscode-info') } @@ -115,9 +147,14 @@ export class ResourceNode implements TreeNode { private getResourceId(): ResourceTypeId { switch (this.type) { case SERVERLESS_FUNCTION_TYPE: + if (this.deployedResource) { + return ResourceTypeId.DeployedFunction + } return ResourceTypeId.Function case 'Api': return ResourceTypeId.Api + case SERVERLESS_CAPACITY_PROVIDER_TYPE: + return ResourceTypeId.CapacityProvider default: return ResourceTypeId.Other } diff --git a/packages/core/src/awsService/appBuilder/explorer/samProject.ts b/packages/core/src/awsService/appBuilder/explorer/samProject.ts index 722ec323192..eedf19ccdc5 100644 --- a/packages/core/src/awsService/appBuilder/explorer/samProject.ts +++ b/packages/core/src/awsService/appBuilder/explorer/samProject.ts @@ -21,18 +21,40 @@ export interface SamAppLocation { projectRoot: vscode.Uri } -export interface ResourceTreeEntity { +export interface BaseResourceEntity { Id: string Type: string +} + +export interface EventEntity extends BaseResourceEntity { + Path?: string + Method?: string +} + +export interface FunctionResourceEntity extends BaseResourceEntity { Runtime?: string CodeUri?: string Handler?: string - Events?: ResourceTreeEntity[] - Path?: string - Method?: string + Events?: EventEntity[] Environment?: { Variables: Record } + CapacityProviderConfig?: string + Architectures?: string +} + +export interface CapacityProviderResourceEntity extends BaseResourceEntity { + Architectures?: string +} + +export type ResourceTreeEntity = FunctionResourceEntity | CapacityProviderResourceEntity | BaseResourceEntity + +export function isFunctionResource(resource: ResourceTreeEntity): resource is FunctionResourceEntity { + return resource.Type === CloudFormation.SERVERLESS_FUNCTION_TYPE +} + +export function isCapacityProviderResource(resource: ResourceTreeEntity): resource is CapacityProviderResourceEntity { + return resource.Type === CloudFormation.SERVERLESS_CAPACITY_PROVIDER_TYPE } export async function getStackName(projectRoot: vscode.Uri): Promise { @@ -77,30 +99,58 @@ function getResourceEntity(template: any): ResourceTreeEntity[] { const resourceTree: ResourceTreeEntity[] = [] for (const [logicalId, resource] of Object.entries(template?.Resources ?? []) as [string, any][]) { - const resourceEntity: ResourceTreeEntity = { - Id: logicalId, - Type: resource.Type, + const resourceEntity = createResourceEntity(logicalId, resource, template) + resourceTree.push(resourceEntity) + } + return resourceTree +} + +function createResourceEntity(logicalId: string, resource: any, template: any): ResourceTreeEntity { + const baseEntity: BaseResourceEntity = { + Id: logicalId, + Type: resource.Type, + } + + // Create type-specific entities + if (resource.Type === CloudFormation.SERVERLESS_FUNCTION_TYPE) { + const functionEntity: FunctionResourceEntity = { + ...baseEntity, Runtime: resource.Properties?.Runtime ?? template?.Globals?.Function?.Runtime, Handler: resource.Properties?.Handler ?? template?.Globals?.Function?.Handler, Events: resource.Properties?.Events ? getEvents(resource.Properties.Events) : undefined, CodeUri: resource.Properties?.CodeUri ?? template?.Globals?.Function?.CodeUri, Environment: resource.Properties?.Environment ?? template?.Globals?.Function?.Environment, + CapacityProviderConfig: + resource.Properties?.CapacityProviderConfig ?? template?.Globals?.Function?.CapacityProviderConfig, + Architectures: resource.Properties?.Architectures?.[0] ?? template?.Globals?.Function?.Architectures?.[0], } - resourceTree.push(resourceEntity) + return functionEntity } - return resourceTree + + if (resource.Type === CloudFormation.SERVERLESS_CAPACITY_PROVIDER_TYPE) { + const capacityProviderEntity: CapacityProviderResourceEntity = { + ...baseEntity, + Architectures: + resource.Properties?.InstanceRequirements?.Architectures?.[0] ?? + template?.Globals?.CapacityProvider?.InstanceRequirements?.Architectures?.[0], + } + return capacityProviderEntity + } + + // Generic resource for unsupported types + return baseEntity } -function getEvents(events: Record): ResourceTreeEntity[] { - const eventResources: ResourceTreeEntity[] = [] +function getEvents(events: Record): EventEntity[] { + const eventResources: EventEntity[] = [] for (const [eventsLogicalId, event] of Object.entries(events)) { const eventProperties = event.Properties - const eventResource: ResourceTreeEntity = { + const eventResource: EventEntity = { Id: eventsLogicalId, Type: event.Type, - Path: eventProperties.Path, - Method: eventProperties.Method, + Path: eventProperties?.Path, + Method: eventProperties?.Method, } eventResources.push(eventResource) } diff --git a/packages/core/src/awsService/appBuilder/lambda2sam/lambda2sam.ts b/packages/core/src/awsService/appBuilder/lambda2sam/lambda2sam.ts index 20f7c372583..75722842bc5 100644 --- a/packages/core/src/awsService/appBuilder/lambda2sam/lambda2sam.ts +++ b/packages/core/src/awsService/appBuilder/lambda2sam/lambda2sam.ts @@ -26,7 +26,7 @@ import { import { downloadUnzip, getLambdaClient, getCFNClient, isPermissionError } from '../utils' import { openProjectInWorkspace } from '../walkthrough' import { ToolkitError } from '../../../shared/errors' -import { ResourcesToImport, StackResource } from 'aws-sdk/clients/cloudformation' +import { ResourceToImport, StackResource } from '@aws-sdk/client-cloudformation' import { SignatureV4 } from '@smithy/signature-v4' import { Sha256 } from '@aws-crypto/sha256-js' import { getIAMConnection } from '../../../auth/utils' @@ -79,7 +79,7 @@ export async function lambdaToSam(lambdaNode: LambdaFunctionNode): Promise progress.report({ increment: 30, message: 'Generating template...' }) // 2.1 call api to get CFN let cfnTemplate: Template - let resourcesToImport: ResourcesToImport + let resourcesToImport: ResourceToImport[] try { ;[cfnTemplate, resourcesToImport] = await callExternalApiForCfnTemplate(lambdaNode) } catch (error) { @@ -282,7 +282,7 @@ export function ifSamTemplate(template: Template): boolean { */ export async function callExternalApiForCfnTemplate( lambdaNode: LambdaFunctionNode -): Promise<[Template, ResourcesToImport]> { +): Promise<[Template, ResourceToImport[]]> { const conn = await getIAMConnection() if (!conn || conn.type !== 'iam') { return [{}, []] @@ -333,7 +333,7 @@ export async function callExternalApiForCfnTemplate( let status: string | undefined = 'CREATE_IN_PROGRESS' let getGeneratedTemplateResponse - let resourcesToImport: ResourcesToImport = [] + let resourcesToImport: ResourceToImport[] = [] const cfn = await getCFNClient(lambdaNode.regionCode) // Wait for template generation to complete @@ -438,7 +438,7 @@ async function promptForProjectLocation(): Promise { */ export async function deployCfnTemplate( template: Template, - resourcesToImport: ResourcesToImport, + resourcesToImport: ResourceToImport[], stackName: string, region: string ): Promise { @@ -901,13 +901,13 @@ export async function getPhysicalIdfromCFNResourceName( // Find resources that start with the given name (SAM transform often adds suffixes) const matchingResources = resources.StackResources.filter((resource: StackResource) => - resource.LogicalResourceId.startsWith(name) + resource.LogicalResourceId?.startsWith(name) ) if (matchingResources.length === 0) { // Try a more flexible approach - check if the resource name is a substring const substringMatches = resources.StackResources.filter((resource: StackResource) => - resource.LogicalResourceId.includes(name) + resource.LogicalResourceId?.includes(name) ) if (substringMatches.length === 0) { @@ -926,7 +926,8 @@ export async function getPhysicalIdfromCFNResourceName( // If we have multiple matches, prefer exact prefix match // Sort by length to get the closest match (shortest additional suffix) matchingResources.sort( - (a: StackResource, b: StackResource) => a.LogicalResourceId.length - b.LogicalResourceId.length + (a: StackResource, b: StackResource) => + (a.LogicalResourceId?.length ?? 0) - (b.LogicalResourceId?.length ?? 0) ) const bestMatch = matchingResources[0] diff --git a/packages/core/src/awsService/appBuilder/utils.ts b/packages/core/src/awsService/appBuilder/utils.ts index bdaa6293b30..bf65059ebc1 100644 --- a/packages/core/src/awsService/appBuilder/utils.ts +++ b/packages/core/src/awsService/appBuilder/utils.ts @@ -8,6 +8,7 @@ import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider' import * as nls from 'vscode-nls' import { ResourceNode } from './explorer/nodes/resourceNode' import type { SamAppLocation } from './explorer/samProject' +import { isFunctionResource } from './explorer/samProject' import { ToolkitError } from '../../shared/errors' import globals from '../../shared/extensionGlobals' import { OpenTemplateParams, OpenTemplateWizard } from './explorer/openTemplate' @@ -21,8 +22,51 @@ import { RuntimeFamily, getFamily } from '../../lambda/models/samLambdaRuntime' import { showMessage } from '../../shared/utilities/messages' import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' import AdmZip from 'adm-zip' -import { CloudFormation, Lambda } from 'aws-sdk' +import { + CloudFormationClient, + CreateChangeSetCommand, + CreateChangeSetInput, + CreateChangeSetOutput, + DescribeChangeSetCommand, + DescribeChangeSetInput, + DescribeChangeSetOutput, + DescribeGeneratedTemplateCommand, + DescribeGeneratedTemplateInput, + DescribeGeneratedTemplateOutput, + DescribeStackResourceCommand, + DescribeStackResourceInput, + DescribeStackResourceOutput, + DescribeStackResourcesCommand, + DescribeStackResourcesInput, + DescribeStackResourcesOutput, + DescribeStacksCommand, + DescribeStacksInput, + DescribeStacksOutput, + ExecuteChangeSetCommand, + ExecuteChangeSetInput, + ExecuteChangeSetOutput, + GetGeneratedTemplateCommand, + GetGeneratedTemplateInput, + GetGeneratedTemplateOutput, + GetTemplateCommand, + GetTemplateInput, + GetTemplateOutput, + waitUntilChangeSetCreateComplete, + waitUntilStackImportComplete, + waitUntilStackUpdateComplete, +} from '@aws-sdk/client-cloudformation' +import { + FunctionConfiguration, + FunctionUrlConfig, + GetFunctionResponse, + GetLayerVersionResponse, + InvocationRequest, + InvocationResponse, + LayerVersionsListItem, + Runtime, +} from '@aws-sdk/client-lambda' import { isAwsError, UnknownError } from '../../shared/errors' +import { WaiterConfiguration } from '@aws-sdk/types' const localize = nls.loadMessageBundle() /** @@ -230,7 +274,7 @@ export class EnhancedLambdaClient { } } - async invoke(name: string, payload?: Lambda.InvocationRequest['Payload']): Promise { + async invoke(name: string, payload?: InvocationRequest['Payload']): Promise { try { return await this.client.invoke(name, payload) } catch (error) { @@ -246,7 +290,7 @@ export class EnhancedLambdaClient { } } - async *listFunctions(): AsyncIterableIterator { + async *listFunctions(): AsyncIterableIterator { try { yield* this.client.listFunctions() } catch (error) { @@ -257,7 +301,7 @@ export class EnhancedLambdaClient { } } - async getFunction(name: string): Promise { + async getFunction(name: string): Promise { try { return await this.client.getFunction(name) } catch (error) { @@ -273,7 +317,7 @@ export class EnhancedLambdaClient { } } - async getLayerVersion(name: string, version: number): Promise { + async getLayerVersion(name: string, version: number): Promise { try { return await this.client.getLayerVersion(name, version) } catch (error) { @@ -289,7 +333,7 @@ export class EnhancedLambdaClient { } } - async *listLayerVersions(name: string): AsyncIterableIterator { + async *listLayerVersions(name: string): AsyncIterableIterator { try { yield* this.client.listLayerVersions(name) } catch (error) { @@ -305,7 +349,7 @@ export class EnhancedLambdaClient { } } - async getFunctionUrlConfigs(name: string): Promise { + async getFunctionUrlConfigs(name: string): Promise { try { return await this.client.getFunctionUrlConfigs(name) } catch (error) { @@ -321,7 +365,7 @@ export class EnhancedLambdaClient { } } - async updateFunctionCode(name: string, zipFile: Uint8Array): Promise { + async updateFunctionCode(name: string, zipFile: Uint8Array): Promise { try { return await this.client.updateFunctionCode(name, zipFile) } catch (error) { @@ -343,13 +387,13 @@ export class EnhancedLambdaClient { */ export class EnhancedCloudFormationClient { constructor( - private readonly client: CloudFormation, + private readonly client: CloudFormationClient, private readonly regionCode: string ) {} - async describeStacks(params: CloudFormation.DescribeStacksInput): Promise { + async describeStacks(params: DescribeStacksInput): Promise { try { - return await this.client.describeStacks(params).promise() + return await this.client.send(new DescribeStacksCommand(params)) } catch (error) { if (isPermissionError(error)) { const stackArn = params.StackName @@ -361,9 +405,9 @@ export class EnhancedCloudFormationClient { } } - async getTemplate(params: CloudFormation.GetTemplateInput): Promise { + async getTemplate(params: GetTemplateInput): Promise { try { - return await this.client.getTemplate(params).promise() + return await this.client.send(new GetTemplateCommand(params)) } catch (error) { if (isPermissionError(error)) { const stackArn = params.StackName @@ -375,9 +419,9 @@ export class EnhancedCloudFormationClient { } } - async createChangeSet(params: CloudFormation.CreateChangeSetInput): Promise { + async createChangeSet(params: CreateChangeSetInput): Promise { try { - return await this.client.createChangeSet(params).promise() + return await this.client.send(new CreateChangeSetCommand(params)) } catch (error) { if (isPermissionError(error)) { const stackArn = params.StackName @@ -389,11 +433,9 @@ export class EnhancedCloudFormationClient { } } - async executeChangeSet( - params: CloudFormation.ExecuteChangeSetInput - ): Promise { + async executeChangeSet(params: ExecuteChangeSetInput): Promise { try { - return await this.client.executeChangeSet(params).promise() + return await this.client.send(new ExecuteChangeSetCommand(params)) } catch (error) { if (isPermissionError(error)) { const stackArn = params.StackName @@ -405,11 +447,9 @@ export class EnhancedCloudFormationClient { } } - async describeChangeSet( - params: CloudFormation.DescribeChangeSetInput - ): Promise { + async describeChangeSet(params: DescribeChangeSetInput): Promise { try { - return await this.client.describeChangeSet(params).promise() + return await this.client.send(new DescribeChangeSetCommand(params)) } catch (error) { if (isPermissionError(error)) { const stackArn = params.StackName @@ -421,11 +461,9 @@ export class EnhancedCloudFormationClient { } } - async describeStackResources( - params: CloudFormation.DescribeStackResourcesInput - ): Promise { + async describeStackResources(params: DescribeStackResourcesInput): Promise { try { - return await this.client.describeStackResources(params).promise() + return await this.client.send(new DescribeStackResourcesCommand(params)) } catch (error) { if (isPermissionError(error)) { const stackArn = params.StackName @@ -437,11 +475,9 @@ export class EnhancedCloudFormationClient { } } - async describeStackResource( - params: CloudFormation.DescribeStackResourceInput - ): Promise { + async describeStackResource(params: DescribeStackResourceInput): Promise { try { - return await this.client.describeStackResource(params).promise() + return await this.client.send(new DescribeStackResourceCommand(params)) } catch (error) { if (isPermissionError(error)) { const stackArn = params.StackName @@ -453,11 +489,9 @@ export class EnhancedCloudFormationClient { } } - async getGeneratedTemplate( - params: CloudFormation.GetGeneratedTemplateInput - ): Promise { + async getGeneratedTemplate(params: GetGeneratedTemplateInput): Promise { try { - return await this.client.getGeneratedTemplate(params).promise() + return await this.client.send(new GetGeneratedTemplateCommand(params)) } catch (error) { if (isPermissionError(error)) { throw createEnhancedPermissionError(error, 'cloudformation', 'getGeneratedTemplate') @@ -466,11 +500,9 @@ export class EnhancedCloudFormationClient { } } - async describeGeneratedTemplate( - params: CloudFormation.DescribeGeneratedTemplateInput - ): Promise { + async describeGeneratedTemplate(params: DescribeGeneratedTemplateInput): Promise { try { - return await this.client.describeGeneratedTemplate(params).promise() + return await this.client.send(new DescribeGeneratedTemplateCommand(params)) } catch (error) { if (isPermissionError(error)) { throw createEnhancedPermissionError(error, 'cloudformation', 'describeGeneratedTemplate') @@ -481,7 +513,20 @@ export class EnhancedCloudFormationClient { async waitFor(state: string, params: any): Promise { try { - return await this.client.waitFor(state as any, params).promise() + const waiterConfig = { + client: this.client, + maxWaitTime: 900, + } satisfies WaiterConfiguration + switch (state) { + case 'changeSetCreateComplete': + return await waitUntilChangeSetCreateComplete(waiterConfig, params) + case 'stackImportComplete': + return await waitUntilStackImportComplete(waiterConfig, params) + case 'stackUpdateComplete': + return await waitUntilStackUpdateComplete(waiterConfig, params) + default: + throw new Error(`Unsupported waiter state: ${state}`) + } } catch (error) { if (isPermissionError(error)) { // For waitFor operations, we'll provide a generic permission error since the specific action varies @@ -508,27 +553,33 @@ export async function runOpenTemplate(arg?: TreeNode) { */ export async function runOpenHandler(arg: ResourceNode): Promise { const folderUri = path.dirname(arg.resource.location.fsPath) - if (!arg.resource.resource.CodeUri) { + const resource = arg.resource.resource + + if (!isFunctionResource(resource)) { + throw new ToolkitError('Resource is not a Lambda function', { code: 'NotAFunction' }) + } + + if (!resource.CodeUri) { throw new ToolkitError('No CodeUri provided in template, cannot open handler', { code: 'NoCodeUriProvided' }) } - if (!arg.resource.resource.Handler) { + if (!resource.Handler) { throw new ToolkitError('No Handler provided in template, cannot open handler', { code: 'NoHandlerProvided' }) } - if (!arg.resource.resource.Runtime) { + if (!resource.Runtime) { throw new ToolkitError('No Runtime provided in template, cannot open handler', { code: 'NoRuntimeProvided' }) } const handlerFile = await getLambdaHandlerFile( vscode.Uri.file(folderUri), - arg.resource.resource.CodeUri, - arg.resource.resource.Handler, - arg.resource.resource.Runtime + resource.CodeUri, + resource.Handler, + resource.Runtime as Runtime ) if (!handlerFile) { throw new ToolkitError( - `No handler file found with name "${arg.resource.resource.Handler}". Ensure the file exists in the expected location."`, + `No handler file found with name "${resource.Handler}". Ensure the file exists in the expected location."`, { code: 'NoHandlerFound', } @@ -560,7 +611,7 @@ export async function getLambdaHandlerFile( folderUri: vscode.Uri, codeUri: string, handler: string, - runtime: string + runtime: Runtime ): Promise { const family = getFamily(runtime) if (!supportedRuntimeForHandler.has(family)) { @@ -704,6 +755,9 @@ export function getLambdaClient(region: string): EnhancedLambdaClient { } export async function getCFNClient(regionCode: string): Promise { - const originalClient = await globals.sdkClientBuilder.createAwsService(CloudFormation, {}, regionCode) + const originalClient = globals.sdkClientBuilderV3.createAwsService({ + serviceClient: CloudFormationClient, + region: regionCode, + }) return new EnhancedCloudFormationClient(originalClient, regionCode) } diff --git a/packages/core/src/awsService/appBuilder/walkthrough.ts b/packages/core/src/awsService/appBuilder/walkthrough.ts index 098e78584f6..e1a0954864b 100644 --- a/packages/core/src/awsService/appBuilder/walkthrough.ts +++ b/packages/core/src/awsService/appBuilder/walkthrough.ts @@ -16,7 +16,7 @@ import { ToolkitError } from '../../shared/errors' import { createSingleFileDialog } from '../../shared/ui/common/openDialog' import { fs } from '../../shared/fs/fs' import path from 'path' -import { telemetry } from '../../shared/telemetry/telemetry' +import { telemetry, ToolId } from '../../shared/telemetry/telemetry' import { minSamCliVersionForAppBuilderSupport } from '../../shared/sam/cli/samCliValidator' import { SamCliInfoInvocation } from '../../shared/sam/cli/samCliInfo' @@ -347,3 +347,34 @@ export async function getOrInstallCliWrapper(toolId: AwsClis, source: string) { } }) } + +export async function installLocalStackExtension(source: string) { + await telemetry.appBuilder_installTool.run(async (span) => { + // TODO: Update `ToolId` accepted values: https://github.com/aws/aws-toolkit-common/blob/8c88537fae2ac7e6524fb2b29ae336c606850eeb/telemetry/definitions/commonDefinitions.json#L2215-L2221 + // @ts-ignore + const toolId: ToolId = 'localstack' + span.record({ source, toolId }) + const extensionId = 'localstack.localstack' + const extension = vscode.extensions.getExtension(extensionId) + if (extension) { + void vscode.window.showInformationMessage( + localize( + 'AWS.toolkit.lambda.walkthrough.localStackExtension.alreadyInstalled', + 'LocalStack extension is already installed' + ) + ) + } else { + try { + await vscode.commands.executeCommand('workbench.extensions.installExtension', extensionId) + void vscode.window.showInformationMessage( + localize( + 'AWS.toolkit.lambda.walkthrough.localStackExtension.installSuccessful', + 'LocalStack extension has been installed' + ) + ) + } catch (err) { + throw ToolkitError.chain(err, 'Failed to install LocalStack extension') + } + } + }) +} diff --git a/packages/core/src/awsService/cloudWatchLogs/activation.ts b/packages/core/src/awsService/cloudWatchLogs/activation.ts index 4c960bb1d03..03cf23c235c 100644 --- a/packages/core/src/awsService/cloudWatchLogs/activation.ts +++ b/packages/core/src/awsService/cloudWatchLogs/activation.ts @@ -23,10 +23,13 @@ import { clearDocument, closeSession, tailLogGroup } from './commands/tailLogGro import { LiveTailDocumentProvider } from './document/liveTailDocumentProvider' import { LiveTailSessionRegistry } from './registry/liveTailSessionRegistry' import { DeployedResourceNode } from '../appBuilder/explorer/nodes/deployedNode' -import { isTreeNode } from '../../shared/treeview/resourceTreeDataProvider' +import { isTreeNode, TreeNode } from '../../shared/treeview/resourceTreeDataProvider' import { getLogger } from '../../shared/logger/logger' import { ToolkitError } from '../../shared/errors' import { LiveTailCodeLensProvider } from './document/liveTailCodeLensProvider' +import { generateLambdaNodeFromResource } from '../appBuilder/explorer/nodes/resourceNode' +import { LambdaFunctionNode } from '../../lambda/explorer/lambdaFunctionNode' +import { getSourceNode } from '../../shared/utilities/treeNodeUtils' export const liveTailRegistry = LiveTailSessionRegistry.instance export const liveTailCodeLensProvider = new LiveTailCodeLensProvider(liveTailRegistry) @@ -132,14 +135,18 @@ export async function activate(context: vscode.ExtensionContext, configuration: await clearDocument(document) }), - Commands.register('aws.appBuilder.searchLogs', async (node: DeployedResourceNode) => { + Commands.register('aws.appBuilder.searchLogs', async (node: DeployedResourceNode | TreeNode) => { try { - const logGroupInfo = isTreeNode(node) - ? { - regionName: node.resource.regionCode, - groupName: getFunctionLogGroupName(node.resource.explorerNode.configuration), - } - : undefined + let tmpNode: LambdaFunctionNode | undefined = getSourceNode(node) + if (!tmpNode && isTreeNode(node)) { + // failed to extract, meaning this is appbuilder function node + tmpNode = await generateLambdaNodeFromResource(node.resource as any) + } + const logGroupInfo = { + regionName: tmpNode.regionCode, + groupName: getFunctionLogGroupName(tmpNode.configuration), + } + const source: string = logGroupInfo ? 'AppBuilderSearchLogs' : 'CommandPaletteSearchLogs' await searchLogGroup(registry, source, logGroupInfo) } catch (err) { diff --git a/packages/core/src/awsService/cloudWatchLogs/registry/liveTailSession.ts b/packages/core/src/awsService/cloudWatchLogs/registry/liveTailSession.ts index 6ec785e76c6..a2364f07460 100644 --- a/packages/core/src/awsService/cloudWatchLogs/registry/liveTailSession.ts +++ b/packages/core/src/awsService/cloudWatchLogs/registry/liveTailSession.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode' import * as AWS from '@aws-sdk/types' import { CloudWatchLogsClient, + type CloudWatchLogsClientConfig, StartLiveTailCommand, StartLiveTailResponseStream, } from '@aws-sdk/client-cloudwatch-logs' @@ -53,12 +54,17 @@ export class LiveTailSession { this._logGroupArn = configuration.logGroupArn this.logStreamFilter = configuration.logStreamFilter this.logEventFilterPattern = configuration.logEventFilterPattern + const cwlClientProps: CloudWatchLogsClientConfig = { + credentials: configuration.awsCredentials, + region: configuration.region, + customUserAgent: getUserAgent(), + } + const endpointUrl = globals.awsContext.getCredentialEndpointUrl() + if (endpointUrl !== undefined) { + cwlClientProps.endpoint = endpointUrl + } this.liveTailClient = { - cwlClient: new CloudWatchLogsClient({ - credentials: configuration.awsCredentials, - region: configuration.region, - customUserAgent: getUserAgent(), - }), + cwlClient: new CloudWatchLogsClient(cwlClientProps), abortController: new AbortController(), } this._maxLines = LiveTailSession.settings.get('limit', 10000) diff --git a/packages/core/src/awsService/cloudformation/artifacts/awsDocumentationLinks.ts b/packages/core/src/awsService/cloudformation/artifacts/awsDocumentationLinks.ts new file mode 100644 index 00000000000..9da617f518c --- /dev/null +++ b/packages/core/src/awsService/cloudformation/artifacts/awsDocumentationLinks.ts @@ -0,0 +1,7 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const ResourceIdentifierDocumentationUrl = + 'https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/resource-identifier.html' diff --git a/packages/core/src/awsService/cloudformation/auth/credentials.ts b/packages/core/src/awsService/cloudformation/auth/credentials.ts new file mode 100644 index 00000000000..95da665c47b --- /dev/null +++ b/packages/core/src/awsService/cloudformation/auth/credentials.ts @@ -0,0 +1,80 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Disposable } from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node' +import { StacksManager } from '../stacks/stacksManager' +import { ResourcesManager } from '../resources/resourcesManager' +import { CloudFormationRegionManager } from '../explorer/regionManager' +import globals from '../../../shared/extensionGlobals' +import * as jose from 'jose' +import * as crypto from 'crypto' + +export const encryptionKey = crypto.randomBytes(32) + +export class AwsCredentialsService implements Disposable { + private authChangeListener: Disposable + private client: LanguageClient | undefined + + constructor( + private readonly stacksManager: StacksManager, + private readonly resourcesManager: ResourcesManager, + private readonly regionManager: CloudFormationRegionManager + ) { + this.authChangeListener = globals.awsContext.onDidChangeContext(() => { + void this.updateCredentialsFromActiveConnection() + }) + } + + async initialize(client: LanguageClient): Promise { + this.client = client + await this.updateCredentialsFromActiveConnection() + } + + private async updateCredentialsFromActiveConnection(): Promise { + if (!this.client) { + return + } + + const credentials = await globals.awsContext.getCredentials() + const profileName = globals.awsContext.getCredentialProfileName() + + if (credentials && profileName) { + const encryptedRequest = await this.createEncryptedCredentialsRequest({ + profile: profileName.replaceAll('profile:', ''), + region: this.regionManager.getSelectedRegion(), + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.sessionToken, + }) + + await this.client.sendRequest('aws/credentials/iam/update', encryptedRequest) + } + + this.stacksManager.clear() + void this.resourcesManager.reload() + } + + async updateRegion(): Promise { + await this.updateCredentialsFromActiveConnection() + } + + private async createEncryptedCredentialsRequest(data: any): Promise { + const payload = new TextEncoder().encode(JSON.stringify({ data })) + + const jwt = await new jose.CompactEncrypt(payload) + .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) + .encrypt(encryptionKey) + + return { + data: jwt, + encrypted: true, + } + } + + dispose(): void { + this.authChangeListener.dispose() + } +} diff --git a/packages/core/src/awsService/cloudformation/cfn-init/cfnEnvironmentApi.ts b/packages/core/src/awsService/cloudformation/cfn-init/cfnEnvironmentApi.ts new file mode 100644 index 00000000000..1df272c175e --- /dev/null +++ b/packages/core/src/awsService/cloudformation/cfn-init/cfnEnvironmentApi.ts @@ -0,0 +1,19 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LanguageClient } from 'vscode-languageclient/node' +import { + ParsedCfnEnvironmentFile, + ParseCfnEnvironmentFilesParams, + ParseCfnEnvironmentFilesRequest, +} from './cfnEnvironmentRequestType' + +export async function parseCfnEnvironmentFiles( + client: LanguageClient, + params: ParseCfnEnvironmentFilesParams +): Promise { + const result = await client.sendRequest(ParseCfnEnvironmentFilesRequest, params) + return result.parsedFiles +} diff --git a/packages/core/src/awsService/cloudformation/cfn-init/cfnEnvironmentManager.ts b/packages/core/src/awsService/cloudformation/cfn-init/cfnEnvironmentManager.ts new file mode 100644 index 00000000000..ba38b004607 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/cfn-init/cfnEnvironmentManager.ts @@ -0,0 +1,267 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Disposable, Uri, window, workspace, commands } from 'vscode' +import { Auth } from '../../../auth/auth' +import { commandKey, extractErrorMessage, formatMessage, toString } from '../utils' +import { + CfnConfig, + CfnEnvironmentConfig, + CfnEnvironmentLookup, + DeploymentConfig, + CfnEnvironmentFileSelectorItem as DeploymentFileDetail, + CfnEnvironmentFileSelectorItem, + unselectedValue, +} from './cfnProjectTypes' +import path from 'path' +import fs from '../../../shared/fs/fs' +import { CfnEnvironmentSelector } from '../ui/cfnEnvironmentSelector' +import { CfnEnvironmentFileSelector } from '../ui/cfnEnvironmentFileSelector' +import globals from '../../../shared/extensionGlobals' +import { TemplateParameter } from '../stacks/actions/stackActionRequestType' +import { validateParameterValue } from '../stacks/actions/stackActionInputValidation' +import { getLogger } from '../../../shared/logger/logger' +import { DocumentInfo } from './cfnEnvironmentRequestType' +import { parseCfnEnvironmentFiles } from './cfnEnvironmentApi' +import { LanguageClient } from 'vscode-languageclient/node' +import { Parameter } from '@aws-sdk/client-cloudformation' +import { + convertRecordToParameters, + convertRecordToTags, + getConfigPath, + getEnvironmentDir, + getProjectDir, +} from './utils' + +export class CfnEnvironmentManager implements Disposable { + private readonly selectedEnvironmentKey = 'aws.cloudformation.selectedEnvironment' + private readonly auth = Auth.instance + private listeners: (() => void)[] = [] + + private readonly initializeOption = 'Initialize Project' + + constructor( + private readonly client: LanguageClient, + private readonly environmentSelector: CfnEnvironmentSelector, + private readonly environmentFileSelector: CfnEnvironmentFileSelector + ) {} + + public addListener(listener: () => void): void { + this.listeners.push(listener) + } + + public getSelectedEnvironmentName(): string | undefined { + return globals.context.workspaceState.get(this.selectedEnvironmentKey) + } + + private notifyListeners(): void { + for (const listener of this.listeners) { + listener() + } + } + + public async promptInitializeIfNeeded(operation: string): Promise { + if (!(await this.isProjectInitialized())) { + const choice = await window.showWarningMessage( + `You must initialize your CFN Project to perform ${operation}`, + this.initializeOption + ) + + if (choice === this.initializeOption) { + void commands.executeCommand(commandKey('init.initializeProject')) + } + return true + } + + return false + } + + public async selectEnvironment(): Promise { + if (await this.promptInitializeIfNeeded('Environment Selection')) { + return + } + + let environmentLookup: CfnEnvironmentLookup + + try { + environmentLookup = await this.fetchAvailableEnvironments() + } catch (error) { + void window.showErrorMessage( + formatMessage(`Failed to retrieve environments from configuration: ${toString(error)}`) + ) + return + } + + const environmentName = await this.environmentSelector.selectEnvironment(environmentLookup) + + if (environmentName) { + await this.setSelectedEnvironment(environmentName, environmentLookup) + } + } + + private async isProjectInitialized(): Promise { + const configPath = await getConfigPath() + const projectDirectory = await getProjectDir() + + return (await fs.existsFile(configPath)) && (await fs.existsDir(projectDirectory)) + } + + private async setSelectedEnvironment( + environmentName: string, + environmentLookup: CfnEnvironmentLookup + ): Promise { + const environment = environmentLookup[environmentName] + + if (environment) { + await globals.context.workspaceState.update(this.selectedEnvironmentKey, environmentName) + + await this.syncEnvironmentWithProfile(environment) + } else { + await globals.context.workspaceState.update(this.selectedEnvironmentKey, undefined) + } + + this.notifyListeners() + } + + private async syncEnvironmentWithProfile(environment: CfnEnvironmentConfig) { + const profileName = environment.profile + + const currentConnection = await this.auth.getConnection({ id: `profile:${profileName}` }) + + if (!currentConnection) { + void window.showErrorMessage(formatMessage(`No connection found for profile: ${profileName}`)) + return + } + + await this.auth.useConnection(currentConnection) + } + + public async fetchAvailableEnvironments(): Promise { + const configPath = await getConfigPath() + const config = JSON.parse(await fs.readFileText(configPath)) as CfnConfig + + return config.environments + } + + public async selectEnvironmentFile( + templateUri: string, + requiredParameters: TemplateParameter[] + ): Promise { + const environmentName = this.getSelectedEnvironmentName() + const selectorItems: CfnEnvironmentFileSelectorItem[] = [] + + if (!environmentName) { + return undefined + } + + try { + const environmentDir = await getEnvironmentDir(environmentName) + const files = await fs.readdir(environmentDir) + + const filesToParse: DocumentInfo[] = await Promise.all( + files + .filter( + ([fileName]) => + fileName.endsWith('.json') || fileName.endsWith('.yaml') || fileName.endsWith('.yml') + ) + .map(async ([fileName]) => { + const filePath = path.join(environmentDir, fileName) + const content = await fs.readFileText(filePath) + const type = fileName.endsWith('.json') ? 'JSON' : 'YAML' + + return { + type, + content, + fileName, + } + }) + ) + + const environmentFiles = await parseCfnEnvironmentFiles(this.client, { documents: filesToParse }) + + for (const deploymentFile of environmentFiles) { + const item = await this.createEnvironmentFileSelectorItem( + deploymentFile.fileName, + deploymentFile.deploymentConfig, + requiredParameters, + templateUri + ) + if (item) { + selectorItems.push(item) + } + } + } catch (error) { + void window.showErrorMessage(`Error loading deployment files: ${extractErrorMessage(error)}`) + return undefined + } + + return await this.environmentFileSelector.selectEnvironmentFile(selectorItems, requiredParameters.length) + } + + public async refreshSelectedEnvironment() { + const environmentName = this.getSelectedEnvironmentName() + const availableEnvironments = await this.fetchAvailableEnvironments() + + // unselect environment if an environment was manually deleted + if (environmentName && !availableEnvironments[environmentName]) { + await this.setSelectedEnvironment(unselectedValue, availableEnvironments) + + return undefined + } + } + + private async createEnvironmentFileSelectorItem( + fileName: string, + deploymentConfig: DeploymentConfig, + requiredParameters: TemplateParameter[], + templateUri: string + ): Promise { + try { + return { + fileName: fileName, + hasMatchingTemplatePath: + workspace.asRelativePath(Uri.parse(templateUri)) === deploymentConfig.templateFilePath, + compatibleParameters: this.getCompatibleParams(deploymentConfig, requiredParameters), + optionalFlags: { + tags: deploymentConfig.tags ? convertRecordToTags(deploymentConfig.tags) : undefined, + includeNestedStacks: deploymentConfig.includeNestedStacks, + importExistingResources: deploymentConfig.importExistingResources, + onStackFailure: deploymentConfig.onStackFailure, + }, + } + } catch (error) { + getLogger().warn(`Failed to create selector item ${fileName}:`, error) + } + } + + private getCompatibleParams( + deploymentConfig: DeploymentConfig, + requiredParameters: TemplateParameter[] + ): Parameter[] | undefined { + if (deploymentConfig.parameters && requiredParameters.length > 0) { + const parameters = deploymentConfig.parameters + + // Filter only parameters that are in template and are valid + const validParams = requiredParameters.filter((templateParam) => { + if (!(templateParam.name in parameters)) { + return false + } + const value = parameters[templateParam.name] + return validateParameterValue(value, templateParam) === undefined + }) + + const validParameterNames = validParams.map((p) => p.name) + const filteredParameters = Object.fromEntries( + Object.entries(parameters).filter(([key]) => validParameterNames.includes(key)) + ) + + return convertRecordToParameters(filteredParameters) + } + } + + dispose(): void { + // No resources to dispose + } +} diff --git a/packages/core/src/awsService/cloudformation/cfn-init/cfnEnvironmentRequestType.ts b/packages/core/src/awsService/cloudformation/cfn-init/cfnEnvironmentRequestType.ts new file mode 100644 index 00000000000..cb1e930109d --- /dev/null +++ b/packages/core/src/awsService/cloudformation/cfn-init/cfnEnvironmentRequestType.ts @@ -0,0 +1,32 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RequestType } from 'vscode-languageserver-protocol' +import { DeploymentConfig } from './cfnProjectTypes' + +export type DocumentInfo = { + type: 'JSON' | 'YAML' + content: string + fileName: string +} + +export type ParsedCfnEnvironmentFile = { + deploymentConfig: DeploymentConfig + fileName: string +} + +export type ParseCfnEnvironmentFilesParams = { + documents: DocumentInfo[] +} + +export type ParseCfnEnvironmentFilesResult = { + parsedFiles: ParsedCfnEnvironmentFile[] +} + +export const ParseCfnEnvironmentFilesRequest = new RequestType< + ParseCfnEnvironmentFilesParams, + ParseCfnEnvironmentFilesResult, + void +>('aws/cfn/environment/files/parse') diff --git a/packages/core/src/awsService/cloudformation/cfn-init/cfnInitCliCaller.ts b/packages/core/src/awsService/cloudformation/cfn-init/cfnInitCliCaller.ts new file mode 100644 index 00000000000..21ab40a5c3a --- /dev/null +++ b/packages/core/src/awsService/cloudformation/cfn-init/cfnInitCliCaller.ts @@ -0,0 +1,80 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as path from 'path' +import * as vscode from 'vscode' +import { ChildProcess } from '../../../shared/utilities/processUtils' +import { extractErrorMessage } from '../utils' + +export interface EnvironmentOption { + name: string + awsProfile: string + parametersFiles?: string[] +} + +export class CfnInitCliCaller { + private binaryPath: string + + constructor(serverRootDir: string) { + this.binaryPath = path.join(serverRootDir, 'bin', 'cfn-init') + } + + async createProject( + projectName: string, + options?: { + projectPath?: string + environments?: EnvironmentOption[] + } + ) { + const args = ['create', projectName] + + if (options?.projectPath) { + args.push('--project-path', options.projectPath) + } + + if (options?.environments && options.environments.length > 0) { + const environmentConfig = { + environments: options.environments, + } + args.push('--environments', JSON.stringify(environmentConfig)) + } + + return this.executeCommand(args) + } + + async addEnvironments(environments: EnvironmentOption[]) { + const args = ['environment', 'add', '--environments', JSON.stringify({ environments })] + return this.executeCommand(args) + } + + async removeEnvironment(envName: string) { + const args = ['environment', 'remove', envName] + return this.executeCommand(args) + } + + private async executeCommand(args: string[]) { + const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd() + + try { + const result = await ChildProcess.run(this.binaryPath, args, { + spawnOptions: { + cwd, + }, + }) + + if (result.exitCode === 0) { + return { success: true, output: result.stdout || undefined } + } else { + void vscode.window.showWarningMessage( + `cfn init command returned exit code ${result.exitCode}: ${result.stderr} - ${result.stdout} - ${extractErrorMessage(result.error)}` + ) + return { success: false, error: result.stderr || `Process exited with code ${result.exitCode}` } + } + } catch (error) { + void vscode.window.showErrorMessage(`Error executing cfn init command: ${extractErrorMessage(error)}`) + return { success: false, error: error instanceof Error ? error.message : String(error) } + } + } +} diff --git a/packages/core/src/awsService/cloudformation/cfn-init/cfnInitUiInterface.ts b/packages/core/src/awsService/cloudformation/cfn-init/cfnInitUiInterface.ts new file mode 100644 index 00000000000..92447d84e4e --- /dev/null +++ b/packages/core/src/awsService/cloudformation/cfn-init/cfnInitUiInterface.ts @@ -0,0 +1,290 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { CfnInitCliCaller, EnvironmentOption } from './cfnInitCliCaller' +import { Auth } from '../../../auth/auth' +import { promptForConnection } from '../../../auth/utils' +import { getEnvironmentName, getProjectName, getProjectPath } from '../ui/inputBox' +import fs from '../../../shared/fs/fs' +import path from 'path' +import { unselectedValue } from './cfnProjectTypes' + +interface FormState { + projectName?: string + projectPath?: string + environments: EnvironmentOption[] +} + +export class CfnInitUiInterface { + private state: FormState = { environments: [] } + + constructor(private cfnInitService: CfnInitCliCaller) {} + + async promptForCreate() { + try { + // Set default project path with validation + const defaultPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd() + + // Validate default path + try { + await fs.checkPerms(defaultPath, '*w*') + const cfnProjectPath = path.join(defaultPath, 'cfn-project') + const cfnProjectExists = await fs.existsDir(cfnProjectPath) + + // Only use default if it's valid and doesn't have cfn-project + this.state.projectPath = cfnProjectExists ? undefined : defaultPath + } catch { + // Default path is invalid, leave undefined to force user selection + this.state.projectPath = undefined + } + + await this.showForm() + } catch (error) { + void vscode.window.showErrorMessage(`CFN Init failed: ${error}`) + } + } + + private async showForm(): Promise { + const quickPick = vscode.window.createQuickPick() + quickPick.title = 'CFN Init: Initialize Project' + quickPick.placeholder = 'Configure your CloudFormation project' + quickPick.buttons = [{ iconPath: new vscode.ThemeIcon('check'), tooltip: 'Create Project' }] + + return new Promise((resolve) => { + const updateItems = () => { + const items = [ + { + label: `Project Name`, + detail: this.state.projectName || unselectedValue, + }, + { + label: `Project Path`, + detail: this.state.projectPath || unselectedValue, + }, + ] + + // Add environment items + for (const [_index, env] of this.state.environments.entries()) { + items.push({ + label: `Adding Environment: ${env.name}`, + detail: `AWS Profile: ${env.awsProfile}`, + }) + } + + const addEnvItem = { + label: '$(plus) Add Environment (At least one required)', + detail: 'Configure a new deployment environment', + } + items.push(addEnvItem) + + if (this.state.environments.length > 0) { + items.push({ + label: '$(trash) Delete Environment', + detail: 'Remove an existing environment', + }) + } + + const createProjectItem = { + label: '$(check) Create Project', + detail: 'Create the CloudFormation project with current configuration', + } + items.push(createProjectItem) + + quickPick.items = items + + // Highlight first undefined state property + if (!this.state.projectName) { + quickPick.activeItems = [items[0]] + } else if (!this.state.projectPath) { + quickPick.activeItems = [items[1]] + } else if (this.state.environments.length === 0) { + quickPick.activeItems = [addEnvItem] + } else { + quickPick.activeItems = [createProjectItem] + } + } + + updateItems() + + quickPick.onDidAccept(async () => { + const selected = quickPick.selectedItems[0] + if (!selected) { + return + } + + if (selected.label.includes('Project Name')) { + const name = await getProjectName(this.state.projectName) + + if (name) { + this.state.projectName = name + } + } else if (selected.label.includes('Project Path')) { + const currentPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '.' + + const pathInput = await getProjectPath(this.state.projectPath || currentPath) + + if (pathInput !== undefined) { + this.state.projectPath = pathInput.trim() || currentPath + } + } else if (selected.label.includes('Add Environment')) { + await this.addEnvironment() + } else if (selected.label.includes('Delete Environment')) { + await this.deleteEnvironment() + } else if (selected.label.includes('Create Project')) { + if (await this.isFormStateValid()) { + quickPick.hide() + resolve(true) + await this.executeProject() + } + return + } + + updateItems() + quickPick.show() + }) + + quickPick.onDidTriggerButton(async () => { + if (!(await this.isFormStateValid())) { + return + } + quickPick.hide() + resolve(true) + await this.executeProject() + }) + + quickPick.onDidHide(() => resolve(false)) + quickPick.show() + }) + } + + private async isFormStateValid(): Promise { + if (!this.state.projectName) { + void vscode.window.showWarningMessage('Project name is required') + return false + } + if (!this.state.projectPath) { + void vscode.window.showWarningMessage('Project path is required') + return false + } + if (this.state.environments.length === 0) { + void vscode.window.showWarningMessage('At least one environment is required') + return false + } + + return true + } + + async collectEnvironmentConfig(): Promise { + const envName = await getEnvironmentName() + + if (!envName) { + return undefined + } + + const connection = await promptForConnection(Auth.instance, 'iam-only') + if (!connection) { + return undefined + } + + if (connection.type !== 'iam') { + void vscode.window.showErrorMessage('Must select a valid IAM Profile for environment setup') + return undefined + } + + const selectedProfile = connection.id.replace('profile:', '') + + const addParamsFile = await vscode.window.showQuickPick(['Yes', 'No'], { + placeHolder: 'Import parameters files?', + }) + + const environment: EnvironmentOption = { + name: envName, + awsProfile: selectedProfile, + } + + if (addParamsFile === 'Yes') { + const result = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectMany: true, + filters: { 'Parameters Files': ['json', 'yaml', 'yml'] }, + }) + if (result && result.length > 0) { + environment.parametersFiles = result.map((uri) => uri.fsPath) + } + } + + return environment + } + + private async addEnvironment() { + const environment = await this.collectEnvironmentConfig() + if (!environment) { + return + } + + // Check for duplicate names + if (this.state.environments.some((e) => e.name === environment.name)) { + void vscode.window.showErrorMessage('Environment name already exists') + return + } + + this.state.environments.push(environment) + } + + private async deleteEnvironment() { + if (this.state.environments.length === 0) { + return + } + + const envNames = this.state.environments.map((env) => env.name) + const selected = await vscode.window.showQuickPick(envNames, { + placeHolder: 'Select environment to delete', + }) + + if (selected) { + this.state.environments = this.state.environments.filter((env) => env.name !== selected) + } + } + + private async executeProject() { + const progress = vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Creating CFN project...', + cancellable: false, + }, + async (progress) => { + progress.report({ increment: 25, message: 'Creating project...' }) + + const projectPath = this.state.projectPath || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '.' + + const result = await this.cfnInitService.createProject(this.state.projectName!, { + projectPath, + environments: this.state.environments, + }) + + if (!result.success) { + throw new Error(result.error) + } + + progress.report({ increment: 100, message: 'Complete!' }) + } + ) + + await progress + void vscode.window.showInformationMessage(`CFN project '${this.state.projectName}' created!`) + + const openProject = await vscode.window.showQuickPick(['Yes', 'No'], { + placeHolder: 'Open project folder in new window?', + }) + + if (openProject === 'Yes') { + const finalPath = this.state.projectPath || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '.' + const uri = vscode.Uri.file(finalPath) + await vscode.commands.executeCommand('vscode.openFolder', uri, true) + } + } +} diff --git a/packages/core/src/awsService/cloudformation/cfn-init/cfnProjectTypes.ts b/packages/core/src/awsService/cloudformation/cfn-init/cfnProjectTypes.ts new file mode 100644 index 00000000000..59aac49bf85 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/cfn-init/cfnProjectTypes.ts @@ -0,0 +1,41 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OnStackFailure, Parameter } from '@aws-sdk/client-cloudformation' +import { ChangeSetOptionalFlags } from '../stacks/actions/stackActionRequestType' + +export type CfnEnvironmentConfig = { + name: string + profile: string +} + +export type CfnEnvironmentLookup = Record + +export type CfnConfig = { + version: string + project: { + name: string + created: string + } + environments: CfnEnvironmentLookup +} + +export type DeploymentConfig = { + templateFilePath?: string + parameters?: Record + tags?: Record + includeNestedStacks?: boolean + importExistingResources?: boolean + onStackFailure?: OnStackFailure +} + +export type CfnEnvironmentFileSelectorItem = { + fileName: string + hasMatchingTemplatePath?: boolean + compatibleParameters?: Parameter[] + optionalFlags?: ChangeSetOptionalFlags +} + +export const unselectedValue = '-' diff --git a/packages/core/src/awsService/cloudformation/cfn-init/utils.ts b/packages/core/src/awsService/cloudformation/cfn-init/utils.ts new file mode 100644 index 00000000000..91b482a11e0 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/cfn-init/utils.ts @@ -0,0 +1,62 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Parameter, Tag } from '@aws-sdk/client-cloudformation' +import path from 'path' +import { workspace } from 'vscode' + +const cfnProjectPath = 'cfn-project' +const configFile = 'cfn-config.json' +const environmentsDirectory = 'environments' + +export function convertRecordToParameters(parameters: Record): Parameter[] { + return Object.entries(parameters).map(([key, value]) => ({ + ParameterKey: key, + ParameterValue: value, + })) +} + +export function convertRecordToTags(tags: Record): Tag[] { + return Object.entries(tags).map(([key, value]) => ({ + Key: key, + Value: value, + })) +} + +export function convertParametersToRecord(parameters: Parameter[]): Record { + return Object.fromEntries( + parameters + .filter((param) => param.ParameterKey && param.ParameterValue) + .map((param) => [param.ParameterKey!, param.ParameterValue!]) + ) +} + +export function convertTagsToRecord(tags: Tag[]): Record { + return Object.fromEntries(tags.filter((tag) => tag.Key && tag.Value).map((tag) => [tag.Key!, tag.Value!])) +} + +export async function getEnvironmentDir(environmentName: string): Promise { + const workspaceRoot = getWorkspaceRoot() + return path.join(workspaceRoot, cfnProjectPath, environmentsDirectory, environmentName) +} + +export async function getConfigPath(): Promise { + const workspaceRoot = getWorkspaceRoot() + return path.join(workspaceRoot, cfnProjectPath, configFile) +} + +export async function getProjectDir(): Promise { + const workspaceRoot = getWorkspaceRoot() + return path.join(workspaceRoot, cfnProjectPath) +} + +export function getWorkspaceRoot(): string { + const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath + if (!workspaceRoot) { + throw new Error('You must open a workspace to use CFN environment commands') + } + + return workspaceRoot +} diff --git a/packages/core/src/awsService/cloudformation/codelens/stackActionCodeLensProvider.ts b/packages/core/src/awsService/cloudformation/codelens/stackActionCodeLensProvider.ts new file mode 100644 index 00000000000..bcf366890cc --- /dev/null +++ b/packages/core/src/awsService/cloudformation/codelens/stackActionCodeLensProvider.ts @@ -0,0 +1,34 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CancellationToken, CodeLens, CodeLensProvider, Event, EventEmitter, TextDocument } from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node' + +const codeLensRequest = 'textDocument/codeLens' + +export class StackActionCodeLensProvider implements CodeLensProvider { + private readonly _onDidChangeCodeLenses = new EventEmitter() + public readonly onDidChangeCodeLenses: Event = this._onDidChangeCodeLenses.event + + constructor(private readonly client: LanguageClient) {} + + async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + if (token.isCancellationRequested) { + return [] + } + + const result = await this.client.sendRequest( + codeLensRequest, + { textDocument: { uri: document.uri.toString() } }, + token + ) + + return result || [] + } + + refresh(): void { + this._onDidChangeCodeLenses.fire() + } +} diff --git a/packages/core/src/awsService/cloudformation/commands/cfnCommands.ts b/packages/core/src/awsService/cloudformation/commands/cfnCommands.ts new file mode 100644 index 00000000000..be5b2d7f60f --- /dev/null +++ b/packages/core/src/awsService/cloudformation/commands/cfnCommands.ts @@ -0,0 +1,996 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + commands, + env, + Uri, + window, + workspace, + Range, + Selection, + TextEditorRevealType, + ProgressLocation, + Disposable, +} from 'vscode' +import { commandKey, extractErrorMessage, findParameterDescriptionPosition, isStackInTransientState } from '../utils' +import { handleLspError } from '../utils/onlineErrorHandler' +import { LanguageClient } from 'vscode-languageclient/node' +import { Command } from 'vscode-languageclient/node' +import * as yaml from 'js-yaml' + +import { Deployment } from '../stacks/actions/deploymentWorkflow' +import { Parameter, Capability, OnStackFailure, Stack, StackStatus } from '@aws-sdk/client-cloudformation' +import { + getParameterValues, + getStackName, + getTemplatePath, + confirmCapabilities, + shouldImportResources, + getResourcesToImport, + getEnvironmentName, + chooseOptionalFlagSuggestion as chooseOptionalFlagMode, + getTags, + getOnStackFailure, + getIncludeNestedStacks, + getImportExistingResources, + getDeploymentMode, + shouldUploadToS3, + getS3Bucket, + getS3Key, + shouldSaveFlagsToFile, + getFilePath, +} from '../ui/inputBox' +import { DiffWebviewProvider } from '../ui/diffWebviewProvider' +import { showErrorMessage } from '../ui/message' +import { getLastValidation, setLastValidation, Validation } from '../stacks/actions/validationWorkflow' +import { + getParameters, + getCapabilities, + getTemplateResources, + getTemplateArtifacts, + describeChangeSet, +} from '../stacks/actions/stackActionApi' +import { + ChangeSetOptionalFlags, + OptionalFlagMode, + TemplateParameter, + ResourceToImport, + ChangeSetReference, + DeploymentMode, +} from '../stacks/actions/stackActionRequestType' +import { ResourceNode } from '../explorer/nodes/resourceNode' +import { ResourcesManager } from '../resources/resourcesManager' +import { RelatedResourcesManager } from '../relatedResources/relatedResourcesManager' +import { DocumentManager } from '../documents/documentManager' +import { CfnEnvironmentManager } from '../cfn-init/cfnEnvironmentManager' + +import { StackOverviewWebviewProvider } from '../ui/stackOverviewWebviewProvider' +import { StackOutputsWebviewProvider } from '../ui/stackOutputsWebviewProvider' +import { StackResourcesWebviewProvider } from '../ui/stackResourcesWebviewProvider' +import { StackViewCoordinator } from '../ui/stackViewCoordinator' +import { ResourceContextValue } from '../explorer/contextValue' +import { getLogger } from '../../../shared/logger/logger' +import { CloudFormationExplorer } from '../explorer/explorer' +import { StacksNode } from '../explorer/nodes/stacksNode' +import { StackNode } from '../explorer/nodes/stackNode' +import { ResourcesNode } from '../explorer/nodes/resourcesNode' +import { ResourceTypeNode } from '../explorer/nodes/resourceTypeNode' +import { StackChangeSetsNode } from '../explorer/nodes/stackChangeSetsNode' +import { CfnInitCliCaller } from '../cfn-init/cfnInitCliCaller' +import { CfnInitUiInterface } from '../cfn-init/cfnInitUiInterface' +import { ChangeSetDeletion } from '../stacks/actions/changeSetDeletionWorkflow' +import { fs } from '../../../shared/fs/fs' +import { convertParametersToRecord, convertTagsToRecord, getEnvironmentDir } from '../cfn-init/utils' +import { DescribeStackRequest } from '../stacks/actions/stackActionProtocol' +import { ResourceIdentifierDocumentationUrl } from '../artifacts/awsDocumentationLinks' +import { CfnEnvironmentFileSelectorItem } from '../cfn-init/cfnProjectTypes' + +export function deployTemplateFromStacksMenuCommand() { + return commands.registerCommand(commandKey('api.deployTemplateFromStacksMenu'), async () => { + return commands.executeCommand(commandKey('api.deployTemplate')) + }) +} + +export function executeChangeSetCommand(client: LanguageClient, coordinator: StackViewCoordinator) { + return commands.registerCommand( + commandKey('api.executeChangeSet'), + async (stackName: string, changeSetName: string) => { + try { + const deployment = new Deployment(stackName, changeSetName, client, coordinator) + + await deployment.deploy() + } catch (error) { + await handleLspError(error, 'Error executing change set') + } + } + ) +} + +export function deleteChangeSetCommand(client: LanguageClient) { + return commands.registerCommand(commandKey('stacks.deleteChangeSet'), async (params: ChangeSetReference) => { + try { + const changeSetDeletion = new ChangeSetDeletion(params.stackName, params.changeSetName, client) + + await changeSetDeletion.delete() + } catch (error) { + await handleLspError(error, 'Error deleting change set') + } + }) +} + +export function viewChangeSetCommand(client: LanguageClient, diffProvider: DiffWebviewProvider) { + return commands.registerCommand(commandKey('stacks.viewChangeSet'), async (params: ChangeSetReference) => { + try { + const describeChangeSetResult = await describeChangeSet(client, { + changeSetName: params.changeSetName, + stackName: params.stackName, + }) + + void diffProvider.updateData( + params.stackName, + describeChangeSetResult.changes, + params.changeSetName, + true, + [], + describeChangeSetResult.deploymentMode, + describeChangeSetResult.status + ) + void commands.executeCommand(commandKey('diff.focus')) + } catch (error) { + await handleLspError(error, 'Error viewing change set') + } + }) +} + +export function deployTemplateCommand( + client: LanguageClient, + diffProvider: DiffWebviewProvider, + documentManager: DocumentManager, + environmentManager: CfnEnvironmentManager +) { + return commands.registerCommand(commandKey('api.deployTemplate'), async (changeSetParams?: string | StackNode) => { + try { + const result = await changeSetSteps( + client, + documentManager, + environmentManager, + false, + typeof changeSetParams === 'string' ? changeSetParams : undefined, + typeof changeSetParams === 'object' ? changeSetParams?.stack.StackName : undefined + ) + if (!result) { + return + } + + const validation = new Validation( + result.templateUri, + result.stackName, + client, + diffProvider, + result.parameters, + result.capabilities, + result.resourcesToImport, + true, // Confirm deployment following successful validation + result.optionalFlags, + result.s3Bucket, + result.s3Key + ) + + setLastValidation(validation) + + await validation.validate() + } catch (error) { + await handleLspError(error, 'Error deploying template') + } + }) +} + +async function promptForResourceImport(client: LanguageClient, templateUri: string) { + const importMode = await shouldImportResources() + let resourcesToImport + if (importMode) { + const templateResources = await getTemplateResources(client, templateUri) + if (!templateResources || templateResources.length === 0) { + showErrorMessage('No resources found in template to import') + return + } + + resourcesToImport = await getResourcesToImport(templateResources) + if (!resourcesToImport || resourcesToImport.length === 0) { + return + } + } + return resourcesToImport +} + +type OptionalFlagSelection = ChangeSetOptionalFlags & { + shouldSaveOptions?: boolean +} + +function isRevertDriftEligible( + stackDetails: Stack | undefined, + importExistingResources: boolean | undefined, + includeNestedStacks: boolean | undefined, + onStackFailure: OnStackFailure | undefined +): boolean { + const isCreate = !stackDetails || stackDetails.StackStatus === StackStatus.REVIEW_IN_PROGRESS + const hasDisableRollback = onStackFailure === OnStackFailure.DO_NOTHING + + return !isCreate && !importExistingResources && !includeNestedStacks && !hasDisableRollback +} + +export async function promptForOptionalFlags( + fileFlags?: ChangeSetOptionalFlags, + stackDetails?: Stack +): Promise { + if (fileFlags && Object.values(fileFlags).every((v) => v !== undefined)) { + return { + ...fileFlags, + shouldSaveOptions: false, + } + } + + let optionalFlags: OptionalFlagSelection | undefined + + const optionSelection = await chooseOptionalFlagMode() + + switch (optionSelection) { + case OptionalFlagMode.Skip: + optionalFlags = { + onStackFailure: fileFlags?.onStackFailure, + includeNestedStacks: fileFlags?.includeNestedStacks, + tags: fileFlags?.tags, + importExistingResources: fileFlags?.importExistingResources, + // default to REVERT_DRIFT if possible because it's generally useful + deploymentMode: + fileFlags?.deploymentMode ?? + (isRevertDriftEligible( + stackDetails, + fileFlags?.importExistingResources, + fileFlags?.includeNestedStacks, + fileFlags?.onStackFailure + ) + ? DeploymentMode.REVERT_DRIFT + : undefined), + shouldSaveOptions: false, + } + + break + case OptionalFlagMode.Input: { + // Only available for UPDATE stack and is incompatible with the other options + const deploymentMode = + fileFlags?.deploymentMode ?? + (isRevertDriftEligible(stackDetails, undefined, undefined, undefined) + ? await getDeploymentMode() + : undefined) + + let onStackFailure: OnStackFailure | undefined + let includeNestedStacks: boolean | undefined + let importExistingResources: boolean | undefined + + if (deploymentMode !== DeploymentMode.REVERT_DRIFT) { + onStackFailure = fileFlags?.onStackFailure ?? (await getOnStackFailure(stackDetails)) + includeNestedStacks = fileFlags?.includeNestedStacks ?? (await getIncludeNestedStacks()) + importExistingResources = fileFlags?.importExistingResources ?? (await getImportExistingResources()) + } + + optionalFlags = { + onStackFailure, + includeNestedStacks, + tags: fileFlags?.tags ?? (await getTags(stackDetails?.Tags)), + importExistingResources, + deploymentMode, + } + + if (!fileFlags && Object.values(optionalFlags).some((val) => val !== undefined)) { + optionalFlags.shouldSaveOptions = true + } + + break + } + case OptionalFlagMode.DevFriendly: + optionalFlags = { + onStackFailure: OnStackFailure.DO_NOTHING, + includeNestedStacks: true, + tags: fileFlags?.tags ?? (await getTags(stackDetails?.Tags)), + importExistingResources: true, + deploymentMode: undefined, + } + + if (!fileFlags && optionalFlags.tags) { + optionalFlags.shouldSaveOptions = true + } + + break + default: + optionalFlags = undefined + } + + return optionalFlags +} + +export async function promptToSaveToFile( + environmentDir: string, + optionalFlags?: ChangeSetOptionalFlags, + parameters?: Parameter[] +): Promise { + const shouldSave = await shouldSaveFlagsToFile() + + if (!shouldSave) { + return + } + + const filePath = await getFilePath(environmentDir) + + if (!filePath) { + return + } + + const data = { + parameters: parameters ? convertParametersToRecord(parameters) : undefined, + tags: optionalFlags?.tags ? convertTagsToRecord(optionalFlags?.tags) : undefined, + 'on-stack-failure': optionalFlags?.onStackFailure, + 'include-nested-stacks': optionalFlags?.includeNestedStacks, + 'import-existing-resources': optionalFlags?.importExistingResources, + 'deployment-mode': optionalFlags?.deploymentMode, + } + + // Determine file type and format accordingly + const isJsonFile = filePath.endsWith('.json') + const config = workspace.getConfiguration('editor') + const tabSize = config.get('tabSize', 2) + const insertSpaces = config.get('insertSpaces', true) + let content: string + + try { + if (isJsonFile) { + // JSON allows both tabs and spaces - respect user preference + const indent = insertSpaces ? tabSize : '\t' + content = JSON.stringify(data, undefined, indent) + } else { + // YAML spec requires spaces for indentation - always use spaces + content = yaml.dump(data, { indent: tabSize, noRefs: true, sortKeys: true }) + } + } catch (error) { + showErrorMessage(`Failed to format deployment options: ${extractErrorMessage(error)}`) + return + } + + try { + await fs.writeFile(filePath, content) + void window.showInformationMessage(`options saved to: ${filePath}`) + } catch (error) { + showErrorMessage(`Failed to save deployment options file: ${extractErrorMessage(error)}`) + } +} + +async function validateArtifactPaths(client: LanguageClient, templateUri: string): Promise { + try { + const artifactsResult = await getTemplateArtifacts(client, templateUri) + if (artifactsResult.artifacts.length === 0) { + return false + } + + for (const artifact of artifactsResult.artifacts) { + const artifactPath = artifact.filePath.startsWith('/') + ? artifact.filePath + : Uri.joinPath(Uri.parse(templateUri), '..', artifact.filePath).fsPath + + if (!(await fs.exists(artifactPath))) { + showErrorMessage(`Artifact path does not exist: ${artifact.filePath}`) + return undefined + } + } + return true + } catch (error) { + getLogger().warn(`Failed to check for artifacts: ${error}`) + return false + } +} + +type UserInputtedTemplateParameters = { + templateUri: string + stackName: string + parameters: Parameter[] | undefined + capabilities: Capability[] + resourcesToImport: ResourceToImport[] | undefined + optionalFlags: ChangeSetOptionalFlags | undefined + s3Bucket?: string + s3Key?: string +} + +async function changeSetSteps( + client: LanguageClient, + documentManager: DocumentManager, + environmentManager: CfnEnvironmentManager, + isValidation: boolean, + templateUri: string | undefined, + stackName: string | undefined +): Promise { + try { + await environmentManager.refreshSelectedEnvironment() + } catch (error) { + getLogger().warn(`Failed to refresh selected environment: ${extractErrorMessage(error)}`) + } + + templateUri ??= await getTemplatePath(documentManager) + if (!templateUri) { + return + } + + await ensureFileIsOpen(templateUri) + + // Check for artifacts first + const hasArtifacts = await validateArtifactPaths(client, templateUri) + if (hasArtifacts === undefined) { + return // Error occurred during validation + } + + // Ask user if they want to upload to S3 + let s3Bucket: string | undefined + let s3Key: string | undefined + const uploadChoice = await shouldUploadToS3() + if (uploadChoice === undefined) { + return // User chose to configure settings, exit command + } + if (uploadChoice) { + s3Bucket = await getS3Bucket() + if (!s3Bucket) { + return + } + + const fileName = templateUri.split('/').pop() + const timestamp = Date.now() + const fileNameWithTimestamp = fileName + ? `${fileName.split('.')[0]}-${timestamp}.${fileName.split('.').pop()}` + : `template-${timestamp}.yaml` + s3Key = await getS3Key(fileNameWithTimestamp) + if (!s3Key) { + return + } + } else if (hasArtifacts) { + s3Bucket = await getS3Bucket( + 'S3 bucket is required because template contains artifacts that need to be uploaded to S3' + ) + if (!s3Bucket) { + return + } + } + + if (!stackName) { + if (isValidation) { + stackName = await getStackName(getLastValidation()?.stackName) + } else { + stackName = await getStackName() + } + // User cancelled + if (!stackName) { + return + } + } + + const stackDetails = await getStackDetails(client, stackName) + + const resourcesToImport = await promptForResourceImport(client, templateUri) + + const paramDefinition = await getTemplateParameters(client, templateUri) + let parameters: Parameter[] | undefined + + let environmentFile: CfnEnvironmentFileSelectorItem | undefined + + try { + environmentFile = await environmentManager.selectEnvironmentFile(templateUri, paramDefinition) + } catch (error) { + getLogger().warn(`Failed to select environment file: ${extractErrorMessage(error)}`) + } + + if (paramDefinition.length > 0) { + parameters = environmentFile?.compatibleParameters + + // Prompt for any remaining parameters not provided by file + const providedParamNames = parameters?.map((p) => p.ParameterKey) ?? [] + const remainingParams = paramDefinition.filter((p) => !providedParamNames.includes(p.name)) + + if (remainingParams.length > 0) { + let prefilledParams: Parameter[] | undefined + + if (stackDetails) { + prefilledParams = stackDetails.Parameters + } else if (isValidation) { + prefilledParams = getLastValidation()?.parameters + } + + const additionalParams = await getParameterValues(remainingParams, prefilledParams) + + if (!additionalParams) { + return + } + + parameters = [...(parameters ?? []), ...additionalParams] + } + } + if (paramDefinition.length > 0 && !parameters) { + return + } + + const optionalFlags = await promptForOptionalFlags(environmentFile?.optionalFlags, stackDetails) + const shouldSaveParameters = parameters && parameters.length > 0 && !environmentFile + + let selectedEnvironment: string | undefined + + try { + selectedEnvironment = environmentManager.getSelectedEnvironmentName() + } catch (error) { + getLogger().warn(`Failed to get selected environment: ${extractErrorMessage(error)}`) + } + + if (selectedEnvironment && (shouldSaveParameters || optionalFlags?.shouldSaveOptions)) { + await promptToSaveToFile(await getEnvironmentDir(selectedEnvironment), optionalFlags, parameters) + } + + const capabilitiesResult = await getCapabilities(client, templateUri) + const capabilities = await confirmCapabilities(capabilitiesResult.capabilities) + if (capabilities === undefined) { + return + } // User cancelled + return { templateUri, stackName, parameters, capabilities, resourcesToImport, optionalFlags, s3Bucket, s3Key } +} + +export function rerunValidateAndDeployCommand() { + return commands.registerCommand(commandKey('api.rerunValidateAndDeploy'), async () => { + try { + const lastValidation = getLastValidation() + if (!lastValidation) { + showErrorMessage('No previous validation to rerun') + return + } + await lastValidation.validate() + } catch (error) { + await handleLspError(error, 'Error rerunning validation') + } + }) +} + +async function ensureFileIsOpen(templateUri: string): Promise { + const uri = Uri.parse(templateUri) + const openEditors = window.visibleTextEditors + const isFileOpen = openEditors.some((editor) => editor.document.uri.toString() === uri.toString()) + + if (!isFileOpen) { + try { + const document = await workspace.openTextDocument(uri) + await window.showTextDocument(document) + } catch (error) { + getLogger().warn(`Could not open file: ${error}`) + throw error + } + } +} + +async function getStackDetails(client: LanguageClient, stackName: string) { + let stackDetails: Stack | undefined + + try { + stackDetails = ( + await client.sendRequest(DescribeStackRequest, { + stackName: stackName, + }) + ).stack + } catch (error) { + const errorMessage = extractErrorMessage(error) + + if (!errorMessage.toLowerCase().includes('does not exist')) { + showErrorMessage(`Encountered error while extracting stack details: ${errorMessage}`) + } + } + + return stackDetails +} + +async function getTemplateParameters(client: LanguageClient, templateUri: string): Promise { + try { + const result = await getParameters(client, templateUri) + return result.parameters + } catch (error) { + showErrorMessage(`Error getting template parameters: ${error instanceof Error ? error.message : String(error)}`) + return [] + } +} + +export function addResourceTypesCommand(resourcesManager: ResourcesManager) { + return commands.registerCommand( + commandKey('api.addResourceTypes'), + async () => await resourcesManager.selectResourceTypes() + ) +} + +export function removeResourceTypeCommand(resourcesManager: ResourcesManager) { + return commands.registerCommand( + commandKey('removeResourceType'), + async (node: ResourceTypeNode) => await resourcesManager.removeResourceType(node.typeName) + ) +} + +export function importResourceStateCommand(resourcesManager: ResourcesManager) { + return commands.registerCommand( + commandKey('api.importResourceState'), + async (node?: ResourceNode, selectedNodes?: ResourceNode[]) => { + try { + const nodes = selectedNodes ?? (node ? [node] : []) + const resourceNodes = nodes.filter((n) => n.contextValue === ResourceContextValue) + await resourcesManager.importResourceStates(resourceNodes) + } catch (error) { + await handleLspError(error, 'Error importing resource state') + } + } + ) +} + +export function cloneResourceStateCommand(resourcesManager: ResourcesManager) { + return commands.registerCommand( + commandKey('api.cloneResourceState'), + async (node?: ResourceNode, selectedNodes?: ResourceNode[]) => { + try { + const nodes = selectedNodes ?? (node ? [node] : []) + const resourceNodes = nodes.filter((n) => n.contextValue === ResourceContextValue) + await resourcesManager.cloneResourceStates(resourceNodes) + } catch (error) { + await handleLspError(error, 'Error cloning resource state') + } + } + ) +} + +export const RefreshResourceListCommand: Command = { + title: 'Refresh Resource List', + command: commandKey('api.refreshResourceList'), + arguments: [], +} + +export function copyResourceIdentifierCommand() { + return commands.registerCommand(commandKey('api.copyResourceIdentifier'), async (resourceNode?: ResourceNode) => { + if (resourceNode?.resourceIdentifier) { + await env.clipboard.writeText(resourceNode.resourceIdentifier) + window.setStatusBarMessage(`Resource identifier copied to clipboard`, 3000) + } + }) +} + +export function refreshAllResourcesCommand(resourcesManager: ResourcesManager) { + return commands.registerCommand(commandKey('api.refreshAllResources'), async () => { + try { + await resourcesManager.refreshAllResources() + } catch (error) { + await handleLspError(error, 'Error refreshing resources') + } + }) +} + +export function refreshResourceListCommand(resourcesManager: ResourcesManager, explorer: CloudFormationExplorer) { + return commands.registerCommand(RefreshResourceListCommand.command, async (resourceTypeNode?: ResourceTypeNode) => { + if (!resourceTypeNode) { + const children = await explorer.getChildren() + const resourcesNode = children.find((child) => child instanceof ResourcesNode) as ResourcesNode | undefined + if (!resourcesNode) { + return + } + + const resourceTypeNodes = (await resourcesNode.getChildren()) as ResourceTypeNode[] + if (resourceTypeNodes.length === 0) { + void window.showInformationMessage('No resource types selected') + return + } + + const selected = await window.showQuickPick( + resourceTypeNodes.map((n) => ({ label: n.typeName, node: n })), + { placeHolder: 'Select resource type to refresh' } + ) + + if (!selected) { + return + } + + resourceTypeNode = selected.node + } + + try { + await resourcesManager.refreshResourceList(resourceTypeNode.typeName) + } catch (error) { + await handleLspError(error, 'Error refreshing resource list') + } + }) +} + +export function focusDiffCommand() { + return commands.registerCommand(commandKey('diff.focus'), () => { + void commands.executeCommand('workbench.view.extension.cfn-diff') + }) +} + +export function getStackManagementInfoCommand(resourcesManager: ResourcesManager) { + return commands.registerCommand(commandKey('api.getStackManagementInfo'), async (resourceNode?: ResourceNode) => { + try { + await resourcesManager.getStackManagementInfo(resourceNode) + } catch (error) { + await handleLspError(error, 'Error getting stack management info') + } + }) +} + +export function extractToParameterPositionCursorCommand(client: LanguageClient) { + return commands.registerCommand( + 'aws.cloudformation.extractToParameter.positionCursor', + async ( + documentUri: string, + parameterName: string, + documentType: string, + trackingCommand?: string, + actionType?: string + ) => { + try { + // Track code action acceptance on the server if tracking parameters provided + if (trackingCommand && actionType) { + await client.sendRequest('workspace/executeCommand', { + command: trackingCommand, + arguments: [actionType], + }) + } + + const uri = Uri.parse(documentUri) + const document = await workspace.openTextDocument(uri) + const editor = await window.showTextDocument(document) + + const text = document.getText() + const position = findParameterDescriptionPosition(text, parameterName, documentType) + + if (position) { + editor.selection = new Selection(position, position) + editor.revealRange(new Range(position, position), TextEditorRevealType.InCenter) + } + } catch (error) { + getLogger().error(`Error positioning cursor in parameter description: ${error}`) + } + } + ) +} + +export function loadMoreResourcesCommand(explorer: CloudFormationExplorer) { + return commands.registerCommand(commandKey('api.loadMoreResources'), async (node?: ResourceTypeNode) => { + if (!node) { + const children = await explorer.getChildren() + const resourcesNode = children.find((child) => child instanceof ResourcesNode) as ResourcesNode | undefined + if (!resourcesNode) { + return + } + + const resourceTypeNodes = (await resourcesNode.getChildren()) as ResourceTypeNode[] + const nodesWithMore = resourceTypeNodes.filter((n) => n.contextValue === 'resourceTypeWithMore') + + if (nodesWithMore.length === 0) { + void window.showInformationMessage('No resource types have more resources to load') + return + } + + const selected = await window.showQuickPick( + nodesWithMore.map((n) => ({ label: n.typeName, node: n })), + { placeHolder: 'Select resource type to load more' } + ) + + if (!selected) { + return + } + + node = selected.node + } + + try { + await node.loadMoreResources() + explorer.refresh(node) + } catch (error) { + await handleLspError(error, 'Error loading more resources') + } + }) +} + +export function loadMoreStacksCommand(explorer: CloudFormationExplorer) { + return commands.registerCommand(commandKey('api.loadMoreStacks'), async (node?: StacksNode) => { + if (!node) { + const children = await explorer.getChildren() + node = children.find((child) => child instanceof StacksNode) as StacksNode | undefined + if (!node) { + return + } + } + + if (node.contextValue !== 'stackSectionWithMore') { + void window.showInformationMessage('No more stacks to load') + return + } + + const stacksNode = node + await window.withProgress( + { + location: ProgressLocation.Notification, + title: 'Loading More Stacks', + }, + async () => { + try { + await stacksNode.loadMoreStacks() + explorer.refresh(stacksNode) + } catch (error) { + await handleLspError(error, 'Error loading more stacks') + } + } + ) + }) +} + +export function searchResourceCommand(explorer: CloudFormationExplorer, resourcesManager: ResourcesManager) { + return commands.registerCommand(commandKey('api.searchResource'), async (node: ResourceTypeNode) => { + try { + const identifier = await window.showInputBox({ + prompt: `Enter ${node.label} identifier to add to list`, + placeHolder: 'Resource identifier must match exactly', + }) + + if (!identifier) { + return + } + + const result = await resourcesManager.searchResource(node.label as string, identifier) + + if (result.found) { + void window.showInformationMessage(`${identifier} (${node.label}) has been added to the list`) + explorer.refresh(node) + } else { + const action = await window.showErrorMessage( + `${node.label} with identifier '${identifier}' was not found. The identifier must match exactly.`, + 'See Documentation' + ) + if (action === 'See Documentation') { + void env.openExternal(Uri.parse(ResourceIdentifierDocumentationUrl)) + } + } + } catch (error) { + await handleLspError(error, 'Error searching for resource') + } + }) +} + +export function refreshChangeSetsCommand(explorer: CloudFormationExplorer) { + return commands.registerCommand(commandKey('stacks.refreshChangeSets'), async (node: StackChangeSetsNode) => { + explorer.refresh(node) + }) +} + +export function loadMoreChangeSetsCommand(explorer: CloudFormationExplorer) { + return commands.registerCommand(commandKey('api.loadMoreChangeSets'), async (node: StackChangeSetsNode) => { + try { + await node.loadMoreChangeSets() + explorer.refresh(node) + } catch (error) { + await handleLspError(error, 'Error loading more change sets') + } + }) +} + +export function viewStackCommand( + coordinator: StackViewCoordinator, + overviewProvider: StackOverviewWebviewProvider, + outputsProvider: StackOutputsWebviewProvider, + resourcesProvider: StackResourcesWebviewProvider +) { + return commands.registerCommand(commandKey('stack.view'), async (node?: StackNode) => { + try { + let stackName: string | undefined + + if (node?.stack.StackName) { + stackName = node.stack.StackName + } else { + stackName = await getStackName() + if (!stackName) { + return + } + } + + await coordinator.setStack(stackName) + + await overviewProvider.showStackOverview(stackName) + + const stackStatus = coordinator.currentStackStatus + + await resourcesProvider.updateData(stackName) + + if (stackStatus && !isStackInTransientState(stackStatus)) { + await outputsProvider.showOutputs(stackName) + } + + await commands.executeCommand(commandKey('stack.overview.focus')) + } catch (error) { + await handleLspError(error, 'Error viewing stack') + } + }) +} + +export function createProjectCommand(uiInterface: CfnInitUiInterface) { + return commands.registerCommand(commandKey('init.initializeProject'), async () => { + await uiInterface.promptForCreate() + }) +} + +export function addEnvironmentCommand( + uiInterface: CfnInitUiInterface, + cfnInit: CfnInitCliCaller, + environmentManager: CfnEnvironmentManager +) { + return commands.registerCommand(commandKey('init.addEnvironment'), async () => { + try { + if (await environmentManager.promptInitializeIfNeeded('Environment Addition')) { + return + } + + const environment = await uiInterface.collectEnvironmentConfig() + if (!environment) { + return + } + + const result = await cfnInit.addEnvironments([environment]) + + if (result.success) { + void window.showInformationMessage(`Environment '${environment.name}' added successfully`) + } else { + showErrorMessage(`Failed to add environment: ${result.error}`) + } + } catch (error) { + showErrorMessage(`Error adding environment: ${error}`) + } + }) +} + +export function removeEnvironmentCommand(cfnInit: CfnInitCliCaller, environmentManager: CfnEnvironmentManager) { + return commands.registerCommand(commandKey('init.removeEnvironment'), async () => { + try { + if (await environmentManager.promptInitializeIfNeeded('Environment Deletion')) { + return + } + + // TODO: Show quickpick of environments instead of inputting it + const envName = await getEnvironmentName() + if (!envName) { + return + } + + const confirm = await window.showWarningMessage(`Remove environment '${envName}'?`, 'Remove', 'Cancel') + if (confirm !== 'Remove') { + return + } + + const result = await cfnInit.removeEnvironment(envName) + + await environmentManager.refreshSelectedEnvironment() + if (result.success) { + void window.showInformationMessage(`Environment '${envName}' removed successfully`) + } else { + showErrorMessage(`Failed to remove environment: ${result.error}`) + } + } catch (error) { + showErrorMessage(`Error removing environment: ${error}`) + } + }) +} + +export function addRelatedResourcesCommand(relatedResourcesManager: RelatedResourcesManager) { + return commands.registerCommand(commandKey('api.addRelatedResources'), async (node?: ResourceTypeNode) => { + const selectedResourceType = node?.typeName + await relatedResourcesManager.addRelatedResources(selectedResourceType) + }) +} + +export function selectEnvironmentCommand(explorer: CloudFormationExplorer): Disposable { + return commands.registerCommand(commandKey('environment.select'), async () => { + await explorer.environmentManager.selectEnvironment() + }) +} diff --git a/packages/core/src/awsService/cloudformation/commands/openStackTemplate.ts b/packages/core/src/awsService/cloudformation/commands/openStackTemplate.ts new file mode 100644 index 00000000000..02c7b8ac704 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/commands/openStackTemplate.ts @@ -0,0 +1,126 @@ +/*! +import { getLogger } from '../../../shared/logger' + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { commands, window, workspace, ViewColumn, Position, Range, Selection, ProgressLocation } from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node' +import { RequestType } from 'vscode-languageserver-protocol' +import { commandKey, formatMessage } from '../utils' +import { handleLspError } from '../utils/onlineErrorHandler' +import { getLogger } from '../../../shared/logger/logger' + +interface GetStackTemplateParams { + stackName: string + primaryIdentifier?: string +} + +interface GetStackTemplateResponse { + templateBody: string + lineNumber?: number +} + +const GetStackTemplateRequest = new RequestType( + 'aws/cfn/stack/template' +) + +function isValidStackName(stackName: string): boolean { + // CloudFormation stack names: 1-128 chars, alphanumeric and hyphens, start with letter + const stackNameRegex = /^[a-zA-Z][a-zA-Z0-9-]{0,127}$/ + return stackNameRegex.test(stackName) +} + +export function openStackTemplateCommand(client: LanguageClient) { + return commands.registerCommand( + commandKey('api.openStackTemplate'), + async (stackName: string, primaryIdentifier?: string) => { + if (!stackName) { + void window.showErrorMessage(formatMessage('No stack name provided')) + return + } + + if (!isValidStackName(stackName)) { + void window.showErrorMessage(formatMessage('Invalid stack name format')) + return + } + + await window + .withProgress( + { + location: ProgressLocation.Notification, + title: `Opening template for stack: ${stackName}`, + cancellable: false, + }, + async () => { + try { + const response = await client.sendRequest(GetStackTemplateRequest, { + stackName, + primaryIdentifier, + }) + + if (!response?.templateBody) { + void window.showWarningMessage( + formatMessage(`No template found for stack: ${stackName}`) + ) + return + } + + const doc = await workspace.openTextDocument({ + content: response.templateBody, + language: response.templateBody.trim().startsWith('{') ? 'json' : 'yaml', + }) + + const editor = await window.showTextDocument(doc, ViewColumn.Active) + + if (response.lineNumber !== undefined) { + const line = doc.lineAt(response.lineNumber) + const position = new Position(response.lineNumber, line.text.length) + editor.selection = new Selection(position, position) + editor.revealRange(new Range(position, position)) + } + + return response + } catch (error) { + getLogger().error('Failed to get stack template: %O', { + stackName, + primaryIdentifier, + error: error instanceof Error ? error.message : String(error), + }) + + await handleLspError(error, `Failed to open template for stack: ${stackName}`) + } + } + ) + .then(async (response) => { + if (!response) { + return + } + + const action = await window.showInformationMessage( + 'Template opened. Would you like to save it locally?', + 'Save As...', + 'No Thanks' + ) + + if (action === 'Save As...') { + const extension = response.templateBody.trim().startsWith('{') ? 'json' : 'yaml' + const saveUri = await window.showSaveDialog({ + defaultUri: workspace.workspaceFolders?.[0]?.uri.with({ + path: `${workspace.workspaceFolders[0].uri.path}/${stackName}-template.${extension}`, + }), + filters: { + 'CloudFormation Templates': [extension], + 'All Files': ['*'], + }, + }) + + if (saveUri) { + await workspace.fs.writeFile(saveUri, Buffer.from(response.templateBody, 'utf8')) + void window.showInformationMessage(`Template saved to ${saveUri.fsPath}`) + } + } + }) + } + ) +} diff --git a/packages/core/src/awsService/cloudformation/commands/regionCommands.ts b/packages/core/src/awsService/cloudformation/commands/regionCommands.ts new file mode 100644 index 00000000000..0869b40437e --- /dev/null +++ b/packages/core/src/awsService/cloudformation/commands/regionCommands.ts @@ -0,0 +1,14 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { CloudFormationExplorer } from '../explorer/explorer' +import { commandKey } from '../utils' + +export function selectRegionCommand(explorer: CloudFormationExplorer): vscode.Disposable { + return vscode.commands.registerCommand(commandKey('selectRegion'), async () => { + await explorer.selectRegion() + }) +} diff --git a/packages/core/src/awsService/cloudformation/consoleLinksUtils.ts b/packages/core/src/awsService/cloudformation/consoleLinksUtils.ts new file mode 100644 index 00000000000..67a56fc6eeb --- /dev/null +++ b/packages/core/src/awsService/cloudformation/consoleLinksUtils.ts @@ -0,0 +1,33 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +export function arnToConsoleUrl(arn: string): string { + return `https://console.aws.amazon.com/go/view?arn=${encodeURIComponent(arn)}` +} + +export function arnToConsoleTabUrl(arn: string, tab: 'resources' | 'events' | 'outputs'): string { + const region = arn.split(':')[3] + return `https://${region}.console.aws.amazon.com/cloudformation/home?region=${region}#/stacks/${tab}?stackId=${encodeURIComponent(arn)}` +} + +// Reference link - https://cloudscape.design/foundation/visual-foundation/iconography/ - icon name: external +export function externalLinkSvg(): string { + return `` +} + +export const consoleLinkStyles = ` +.console-link { + display: inline-flex; + align-items: center; + opacity: 0.8; + transition: opacity 0.2s; + line-height: 1; +} +.console-link:hover { + opacity: 1; +} +.console-link svg path { + fill: #007ACC; +} +` diff --git a/packages/core/src/awsService/cloudformation/documents/documentManager.ts b/packages/core/src/awsService/cloudformation/documents/documentManager.ts new file mode 100644 index 00000000000..5232d6c00cb --- /dev/null +++ b/packages/core/src/awsService/cloudformation/documents/documentManager.ts @@ -0,0 +1,44 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NotificationType } from 'vscode-languageserver-protocol' +import { LanguageClient } from 'vscode-languageclient/node' + +export type DocumentMetadata = { + uri: string + fileName: string + ext: string + type: string + cfnType: string + languageId: string + version: number + lineCount: number +} + +const DocumentsMetadataNotification = new NotificationType('aws/documents/metadata') + +type DocumentsChangeListener = (documents: DocumentMetadata[]) => void + +export class DocumentManager { + private documents: DocumentMetadata[] = [] + private readonly listeners: DocumentsChangeListener[] = [] + + constructor(private readonly client: LanguageClient) { + this.client.onNotification(DocumentsMetadataNotification, (documents: DocumentMetadata[]) => { + this.documents = documents + for (const listener of this.listeners) { + listener(this.documents) + } + }) + } + + addListener(listener: DocumentsChangeListener) { + this.listeners.push(listener) + } + + get() { + return [...this.documents] + } +} diff --git a/packages/core/src/awsService/cloudformation/documents/documentPreview.ts b/packages/core/src/awsService/cloudformation/documents/documentPreview.ts new file mode 100644 index 00000000000..208ba7867cd --- /dev/null +++ b/packages/core/src/awsService/cloudformation/documents/documentPreview.ts @@ -0,0 +1,42 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NotificationType } from 'vscode-languageserver-protocol' +import { LanguageClient } from 'vscode-languageclient/node' +import { ViewColumn, window, workspace } from 'vscode' + +type DocumentPreviewType = { + content: string + language: string + viewColumn?: number + preserveFocus?: boolean +} + +const DocumentPreviewNotification = new NotificationType('aws/document/preview') + +export class DocumentPreview { + constructor(private readonly client: LanguageClient) { + this.client.onNotification(DocumentPreviewNotification, (preview: DocumentPreviewType) => { + if (preview) { + void docPreview(preview) + } + }) + } +} + +export async function docPreview(preview: DocumentPreviewType) { + const { content, language, viewColumn = ViewColumn.Beside, preserveFocus = true } = preview + + await window.showTextDocument( + await workspace.openTextDocument({ + content, + language, + }), + { + viewColumn, + preserveFocus, + } + ) +} diff --git a/packages/core/src/awsService/cloudformation/explorer/contextValue.ts b/packages/core/src/awsService/cloudformation/explorer/contextValue.ts new file mode 100644 index 00000000000..0122fa2ffc2 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/explorer/contextValue.ts @@ -0,0 +1,11 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const ResourceSectionContextValue = 'resourceSection' +export const ResourceTypeContextValue = 'resourceType' +export const ResourceTypeWithMoreContextValue = 'resourceTypeWithMore' +export const LoadMoreResourcesContextValue = 'loadMoreResources' +export const ResourceContextValue = 'resource' +export const RegionSelectorContextValue = 'regionSelector' diff --git a/packages/core/src/awsService/cloudformation/explorer/explorer.ts b/packages/core/src/awsService/cloudformation/explorer/explorer.ts new file mode 100644 index 00000000000..1afa1256b72 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/explorer/explorer.ts @@ -0,0 +1,107 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { RegionProvider } from '../../../shared/regions/regionProvider' +import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' +import { PlaceholderNode } from '../../../shared/treeview/nodes/placeholderNode' +import { RefreshableAwsTreeProvider } from '../../../shared/treeview/awsTreeProvider' +import { CloudFormationRegionManager } from './regionManager' +import { StacksNode } from './nodes/stacksNode' +import { ResourcesNode } from './nodes/resourcesNode' +import { RegionSelectorNode } from './nodes/regionSelectorNode' +import { AwsCredentialsService } from '../auth/credentials' +import { getLogger } from '../../../shared/logger/logger' +import { getIcon } from '../../../shared/icons' +import globals from '../../../shared/extensionGlobals' + +import { StacksManager } from '../stacks/stacksManager' +import { ResourcesManager } from '../resources/resourcesManager' + +import { DocumentManager } from '../documents/documentManager' +import { ChangeSetsManager } from '../stacks/changeSetsManager' +import { CfnEnvironmentManager } from '../cfn-init/cfnEnvironmentManager' +import { CfnEnvironmentsNode } from './nodes/cfnEnvironmentsNode' +import { telemetry } from '../../../shared/telemetry/telemetry' +import { cloudFormationUiClickMetric } from '../utils' + +export class CloudFormationExplorer implements vscode.TreeDataProvider, RefreshableAwsTreeProvider { + public viewProviderId: string = 'aws.cloudformation' + public readonly onDidChangeTreeData: vscode.Event + private readonly _onDidChangeTreeData: vscode.EventEmitter + public readonly regionManager: CloudFormationRegionManager + public readonly environmentManager: CfnEnvironmentManager + private credentialsService: AwsCredentialsService | undefined + + public constructor( + private readonly stacksManager: StacksManager, + private readonly resourcesManager: ResourcesManager, + private readonly changeSetsManager: ChangeSetsManager, + documentManager: DocumentManager, + regionProvider: RegionProvider, + environmentManager: CfnEnvironmentManager + ) { + this._onDidChangeTreeData = new vscode.EventEmitter() + this.onDidChangeTreeData = this._onDidChangeTreeData.event + this.regionManager = new CloudFormationRegionManager(regionProvider) + this.environmentManager = environmentManager + } + + public setCredentialsService(credentialsService: AwsCredentialsService): void { + this.credentialsService = credentialsService + } + + public async selectRegion(): Promise { + telemetry.ui_click.emit({ elementId: cloudFormationUiClickMetric }) + const changed = await this.regionManager.showRegionSelector() + if (changed) { + this.refresh() + if (this.credentialsService) { + await this.credentialsService.updateRegion() + } + } + } + + public getTreeItem(element: AWSTreeNodeBase): vscode.TreeItem { + return element + } + + public async getChildren(element?: AWSTreeNodeBase): Promise { + if (!element) { + return this.getRootChildren() + } + telemetry.ui_click.emit({ elementId: cloudFormationUiClickMetric }) + return element.getChildren() + } + + private getRootChildren(): AWSTreeNodeBase[] { + try { + // Show sign-in message when not authenticated + if (!globals.awsContext.getCredentialProfileName()) { + const signInNode = new PlaceholderNode(this as any, 'Sign in to get started') + signInNode.iconPath = getIcon('vscode-account') + signInNode.command = { + command: 'aws.toolkit.login', + title: 'Sign in', + } + return [signInNode] + } + + return [ + new RegionSelectorNode(this.regionManager), + new CfnEnvironmentsNode(this.environmentManager), + new StacksNode(this.stacksManager, this.changeSetsManager), + new ResourcesNode(this.resourcesManager), + ] + } catch (error) { + getLogger().error('CloudFormation explorer error: %O', error) + return [] + } + } + + public refresh(node?: AWSTreeNodeBase): void { + this._onDidChangeTreeData.fire(node) + } +} diff --git a/packages/core/src/awsService/cloudformation/explorer/nodes/cfnEnvironmentsNode.ts b/packages/core/src/awsService/cloudformation/explorer/nodes/cfnEnvironmentsNode.ts new file mode 100644 index 00000000000..4e287158c6e --- /dev/null +++ b/packages/core/src/awsService/cloudformation/explorer/nodes/cfnEnvironmentsNode.ts @@ -0,0 +1,31 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TreeItemCollapsibleState, ThemeIcon } from 'vscode' +import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' +import { CfnEnvironmentManager } from '../../cfn-init/cfnEnvironmentManager' +import { commandKey } from '../../utils' + +export class CfnEnvironmentsNode extends AWSTreeNodeBase { + public constructor(readonly environmentManager: CfnEnvironmentManager) { + const selectedEnv = environmentManager.getSelectedEnvironmentName() + const label = selectedEnv ? `Environment: ${selectedEnv}` : 'Environment: not selected' + + super(label, TreeItemCollapsibleState.None) + this.contextValue = 'environmentsSection' + this.iconPath = new ThemeIcon('settings-gear') + this.tooltip = selectedEnv + ? `Current environment: ${selectedEnv}. Click to select a different environment.` + : 'No environment selected. Click to select an environment.' + this.command = { + command: commandKey('environment.select'), + title: 'Select Environment', + } + } + + public override async getChildren(): Promise { + return [] + } +} diff --git a/packages/core/src/awsService/cloudformation/explorer/nodes/regionSelectorNode.ts b/packages/core/src/awsService/cloudformation/explorer/nodes/regionSelectorNode.ts new file mode 100644 index 00000000000..9766a6a3c4f --- /dev/null +++ b/packages/core/src/awsService/cloudformation/explorer/nodes/regionSelectorNode.ts @@ -0,0 +1,24 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TreeItemCollapsibleState, ThemeIcon } from 'vscode' +import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' +import { CloudFormationRegionManager } from '../regionManager' +import { RegionSelectorContextValue } from '../contextValue' +import { commandKey } from '../../utils' + +export class RegionSelectorNode extends AWSTreeNodeBase { + public constructor(regionManager: CloudFormationRegionManager) { + const currentRegion = regionManager.getSelectedRegion() + super(currentRegion, TreeItemCollapsibleState.None) + this.contextValue = RegionSelectorContextValue + this.iconPath = new ThemeIcon('globe') + this.tooltip = `Current region: ${currentRegion}. Click to select a different region.` + this.command = { + command: commandKey('selectRegion'), + title: 'Select Region', + } + } +} diff --git a/packages/core/src/awsService/cloudformation/explorer/nodes/resourceNode.ts b/packages/core/src/awsService/cloudformation/explorer/nodes/resourceNode.ts new file mode 100644 index 00000000000..d92c52aa3f1 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/explorer/nodes/resourceNode.ts @@ -0,0 +1,21 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TreeItemCollapsibleState } from 'vscode' +import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' + +export class ResourceNode extends AWSTreeNodeBase { + public constructor( + public readonly resourceIdentifier: string, + public readonly resourceType: string + ) { + super(resourceIdentifier, TreeItemCollapsibleState.None) + this.contextValue = 'resource' + } + + public override async getChildren(): Promise { + return [] + } +} diff --git a/packages/core/src/awsService/cloudformation/explorer/nodes/resourceTypeNode.ts b/packages/core/src/awsService/cloudformation/explorer/nodes/resourceTypeNode.ts new file mode 100644 index 00000000000..0968d727198 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/explorer/nodes/resourceTypeNode.ts @@ -0,0 +1,92 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TreeItemCollapsibleState, ThemeIcon } from 'vscode' +import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' +import { ResourceList } from '../../resources/resourceRequestTypes' +import { ResourceNode } from './resourceNode' +import { commandKey } from '../../utils' +import { ResourcesManager } from '../../resources/resourcesManager' +import { + LoadMoreResourcesContextValue, + ResourceTypeContextValue, + ResourceTypeWithMoreContextValue, +} from '../contextValue' + +class LoadMoreResourcesNode extends AWSTreeNodeBase { + public constructor(private readonly parent: ResourceTypeNode) { + super('[Load More...]', TreeItemCollapsibleState.None) + this.contextValue = LoadMoreResourcesContextValue + this.command = { + title: 'Load More', + command: commandKey('api.loadMoreResources'), + arguments: [this.parent], + } + } +} + +class NoResourcesNode extends AWSTreeNodeBase { + public constructor() { + super('No resources found', TreeItemCollapsibleState.None) + this.contextValue = 'noResources' + this.iconPath = new ThemeIcon('info') + } +} + +export class ResourceTypeNode extends AWSTreeNodeBase { + private loaded = false + + public constructor( + public readonly typeName: string, + private readonly resourcesManager: ResourcesManager, + private resourceList?: ResourceList + ) { + super(typeName, TreeItemCollapsibleState.Collapsed) + this.loaded = resourceList !== undefined + this.updateNode() + } + + private updateNode(): void { + if (!this.resourceList) { + this.description = undefined + this.contextValue = ResourceTypeContextValue + return + } + const count = this.resourceList.resourceIdentifiers.length + const hasMore = this.resourceList.nextToken !== undefined + this.description = hasMore ? `(${count}+)` : `(${count})` + this.contextValue = hasMore ? ResourceTypeWithMoreContextValue : ResourceTypeContextValue + } + + public override async getChildren(): Promise { + if (!this.loaded) { + await this.resourcesManager.loadResourceType(this.typeName) + this.resourceList = this.resourcesManager.get().find((r) => r.typeName === this.typeName) + this.loaded = true + this.updateNode() + } + + if (!this.resourceList || this.resourceList.resourceIdentifiers.length === 0) { + return [new NoResourcesNode()] + } + + const nodes = this.resourceList.resourceIdentifiers.map( + (identifier) => new ResourceNode(identifier, this.typeName) + ) + + return this.resourceList.nextToken ? [...nodes, new LoadMoreResourcesNode(this)] : nodes + } + + public async loadMoreResources(): Promise { + if (!this.resourceList?.nextToken) { + return + } + + await this.resourcesManager.loadMoreResources(this.typeName, this.resourceList.nextToken) + + this.resourceList = this.resourcesManager.get().find((r) => r.typeName === this.typeName) + this.updateNode() + } +} diff --git a/packages/core/src/awsService/cloudformation/explorer/nodes/resourcesNode.ts b/packages/core/src/awsService/cloudformation/explorer/nodes/resourcesNode.ts new file mode 100644 index 00000000000..69292f12bb3 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/explorer/nodes/resourcesNode.ts @@ -0,0 +1,26 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TreeItemCollapsibleState } from 'vscode' +import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' +import { ResourcesManager } from '../../resources/resourcesManager' +import { ResourceTypeNode } from './resourceTypeNode' + +export class ResourcesNode extends AWSTreeNodeBase { + public constructor(private readonly resourcesManager: ResourcesManager) { + super('Resources', TreeItemCollapsibleState.Collapsed) + this.contextValue = 'resourceSection' + } + + public override async getChildren(): Promise { + const selectedTypes = this.resourcesManager.getSelectedResourceTypes() + const loadedResources = this.resourcesManager.get() + + return selectedTypes.map((typeName) => { + const resourceList = loadedResources.find((r) => r.typeName === typeName) + return new ResourceTypeNode(typeName, this.resourcesManager, resourceList) + }) + } +} diff --git a/packages/core/src/awsService/cloudformation/explorer/nodes/stackChangeSetsNode.ts b/packages/core/src/awsService/cloudformation/explorer/nodes/stackChangeSetsNode.ts new file mode 100644 index 00000000000..343bf97c880 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/explorer/nodes/stackChangeSetsNode.ts @@ -0,0 +1,109 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TreeItemCollapsibleState, ThemeIcon, ThemeColor } from 'vscode' +import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' +import { ChangeSetsManager } from '../../stacks/changeSetsManager' +import { ChangeSetInfo } from '../../stacks/actions/stackActionRequestType' +import { commandKey } from '../../utils' + +class LoadMoreChangeSetsNode extends AWSTreeNodeBase { + public constructor(private readonly parent: StackChangeSetsNode) { + super('[Load More...]', TreeItemCollapsibleState.None) + this.contextValue = 'loadMoreChangeSets' + this.command = { + title: 'Load More', + command: commandKey('api.loadMoreChangeSets'), + arguments: [this.parent], + } + } +} + +class NoChangeSetsNode extends AWSTreeNodeBase { + public constructor() { + super('No change sets found', TreeItemCollapsibleState.None) + this.contextValue = 'noChangeSets' + this.iconPath = new ThemeIcon('info') + } +} + +export class StackChangeSetsNode extends AWSTreeNodeBase { + public constructor( + private readonly stackName: string, + private readonly changeSetsManager: ChangeSetsManager + ) { + super('Change Sets', TreeItemCollapsibleState.Collapsed) + this.contextValue = 'stackChangeSets' + this.iconPath = new ThemeIcon('diff') + this.updateNode() + } + + private updateNode(): void { + const count = this.changeSetsManager.get(this.stackName).length + const hasMore = this.changeSetsManager.hasMore(this.stackName) + this.description = hasMore ? `(${count}+)` : `(${count})` + this.contextValue = hasMore ? 'stackChangeSetsWithMore' : 'stackChangeSets' + } + + public override async getChildren(): Promise { + const changeSets = await this.changeSetsManager.getChangeSets(this.stackName) + this.updateNode() + + if (changeSets.length === 0) { + return [new NoChangeSetsNode()] + } + + const nodes = changeSets.map((changeSet) => new ChangeSetNode(changeSet, this.stackName)) + return this.changeSetsManager.hasMore(this.stackName) ? [...nodes, new LoadMoreChangeSetsNode(this)] : nodes + } + + public async loadMoreChangeSets(): Promise { + await this.changeSetsManager.loadMoreChangeSets(this.stackName) + this.updateNode() + } +} + +export class ChangeSetNode extends AWSTreeNodeBase { + public readonly stackName: string + public readonly changeSetName: string + + public constructor( + public readonly changeSet: ChangeSetInfo, + stackName: string + ) { + super(changeSet.changeSetName, TreeItemCollapsibleState.None) + this.stackName = stackName + this.changeSetName = changeSet.changeSetName + this.contextValue = 'changeSet' + this.tooltip = `${changeSet.changeSetName} [${changeSet.status}]` + this.iconPath = this.getIconForStatus(changeSet.status) + this.stackName = stackName + this.changeSetName = changeSet.changeSetName + } + + private getIconForStatus(status: string): ThemeIcon { + switch (status) { + case 'CREATE_PENDING': + case 'DELETE_PENDING': + return new ThemeIcon('clock') + case 'CREATE_IN_PROGRESS': + case 'DELETE_IN_PROGRESS': + return new ThemeIcon('sync~spin', new ThemeColor('charts.yellow')) + case 'CREATE_COMPLETE': + return new ThemeIcon('check', new ThemeColor('charts.green')) + case 'DELETE_COMPLETE': + return new ThemeIcon('trash') + case 'DELETE_FAILED': + case 'FAILED': + return new ThemeIcon('error', new ThemeColor('charts.red')) + default: + return new ThemeIcon('git-commit') + } + } + + public override async getChildren(): Promise { + return [] + } +} diff --git a/packages/core/src/awsService/cloudformation/explorer/nodes/stackNode.ts b/packages/core/src/awsService/cloudformation/explorer/nodes/stackNode.ts new file mode 100644 index 00000000000..531f864b3db --- /dev/null +++ b/packages/core/src/awsService/cloudformation/explorer/nodes/stackNode.ts @@ -0,0 +1,46 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TreeItemCollapsibleState, ThemeIcon, ThemeColor } from 'vscode' +import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' +import { StackSummary } from '@aws-sdk/client-cloudformation' +import { StackChangeSetsNode } from './stackChangeSetsNode' +import { ChangeSetsManager } from '../../stacks/changeSetsManager' + +export class StackNode extends AWSTreeNodeBase { + public constructor( + public readonly stack: StackSummary, + private readonly changeSetsManager: ChangeSetsManager + ) { + super(stack.StackName ?? 'Unknown Stack', TreeItemCollapsibleState.Collapsed) + this.contextValue = 'stack' + this.tooltip = `${stack.StackName} [${stack.StackStatus}]` + this.iconPath = this.getStackIcon(stack.StackStatus) + } + + private getStackIcon(status?: string): ThemeIcon { + if (!status) { + return new ThemeIcon('layers') + } + + if (status.includes('COMPLETE') && !status.includes('ROLLBACK')) { + return new ThemeIcon('check', new ThemeColor('charts.green')) + } else if (status.includes('FAILED') || status.includes('ROLLBACK')) { + return new ThemeIcon('error', new ThemeColor('charts.red')) + } else if (status.includes('PROGRESS')) { + return new ThemeIcon('sync~spin', new ThemeColor('charts.yellow')) + } else { + return new ThemeIcon('layers') + } + } + + public override async getChildren(): Promise { + const stackName = this.stack.StackName ?? '' + + await this.changeSetsManager.getChangeSets(stackName) + + return [new StackChangeSetsNode(stackName, this.changeSetsManager)] + } +} diff --git a/packages/core/src/awsService/cloudformation/explorer/nodes/stacksNode.ts b/packages/core/src/awsService/cloudformation/explorer/nodes/stacksNode.ts new file mode 100644 index 00000000000..b601efcc122 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/explorer/nodes/stacksNode.ts @@ -0,0 +1,59 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TreeItemCollapsibleState } from 'vscode' +import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' +import { commandKey } from '../../utils' +import { StacksManager } from '../../stacks/stacksManager' +import { StackSummary } from '@aws-sdk/client-cloudformation' +import { ChangeSetsManager } from '../../stacks/changeSetsManager' +import { StackNode } from './stackNode' + +class LoadMoreStacksNode extends AWSTreeNodeBase { + public constructor(private readonly parent: StacksNode) { + super('[Load More...]', TreeItemCollapsibleState.None) + this.contextValue = 'loadMoreStacks' + this.command = { + title: 'Load More', + command: commandKey('api.loadMoreStacks'), + arguments: [this.parent], + } + } +} + +export class StacksNode extends AWSTreeNodeBase { + public constructor( + private readonly stacksManager: StacksManager, + private readonly changeSetsManager: ChangeSetsManager + ) { + super('Stacks', TreeItemCollapsibleState.Collapsed) + this.updateNode() + } + + public override async getChildren(): Promise { + await this.stacksManager.ensureLoaded() + this.updateNode() + const stacks = this.stacksManager.get() + const nodes = stacks.map((stack: StackSummary) => new StackNode(stack, this.changeSetsManager)) + return this.stacksManager.hasMore() ? [...nodes, new LoadMoreStacksNode(this)] : nodes + } + + private updateNode(): void { + if (this.stacksManager.isLoaded()) { + const count = this.stacksManager.get().length + const hasMore = this.stacksManager.hasMore() + this.description = hasMore ? `(${count}+)` : `(${count})` + this.contextValue = hasMore ? 'stackSectionWithMore' : 'stackSection' + } else { + this.description = undefined + this.contextValue = 'stackSection' + } + } + + public async loadMoreStacks(): Promise { + await this.stacksManager.loadMoreStacks() + this.updateNode() + } +} diff --git a/packages/core/src/awsService/cloudformation/explorer/regionManager.ts b/packages/core/src/awsService/cloudformation/explorer/regionManager.ts new file mode 100644 index 00000000000..6dfdaf9291b --- /dev/null +++ b/packages/core/src/awsService/cloudformation/explorer/regionManager.ts @@ -0,0 +1,70 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as nls from 'vscode-nls' +import { RegionProvider } from '../../../shared/regions/regionProvider' +import globals from '../../../shared/extensionGlobals' + +const localize = nls.loadMessageBundle() + +export class CloudFormationRegionManager { + private static readonly storageKey = 'aws.cloudformation.region' + + constructor(private readonly regionProvider: RegionProvider) {} + + public getSelectedRegion(): string { + const cfnRegion = globals.globalState.tryGet(CloudFormationRegionManager.storageKey, String) + + // If no CloudFormation region selected, use credential default region, then AWS explorer region as fallback + if (!cfnRegion) { + const credentialDefaultRegion = globals.awsContext.getCredentialDefaultRegion() + if (credentialDefaultRegion) { + return credentialDefaultRegion + } + + const awsExplorerRegions = globals.globalState.tryGet('region', Object, []) + return awsExplorerRegions.length > 0 ? awsExplorerRegions[0] : 'us-east-1' + } + + return cfnRegion + } + + public async updateSelectedRegion(region: string): Promise { + await globals.globalState.update(CloudFormationRegionManager.storageKey, region) + } + + public async showRegionSelector(): Promise { + const currentRegion = this.getSelectedRegion() + const allRegions = this.regionProvider.getRegions() + + const items: vscode.QuickPickItem[] = allRegions.map((r) => ({ + label: r.name, + detail: r.id, + })) + + const placeholder = localize( + 'cloudformation.showHideRegionPlaceholder', + 'Select region for CloudFormation panel' + ) + + const result = await vscode.window.showQuickPick(items, { + placeHolder: placeholder, + canPickMany: false, + matchOnDetail: true, + }) + + if (!result || !result.detail) { + return false + } + + if (result.detail !== currentRegion) { + await this.updateSelectedRegion(result.detail) + return true + } + + return false + } +} diff --git a/packages/core/src/awsService/cloudformation/extension.ts b/packages/core/src/awsService/cloudformation/extension.ts new file mode 100644 index 00000000000..22016000d83 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/extension.ts @@ -0,0 +1,345 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ExtensionContext, window, languages, commands, Disposable } from 'vscode' +import { + LanguageClient, + LanguageClientOptions, + ServerOptions, + TransportKind, + ErrorHandlerResult, + CloseHandlerResult, +} from 'vscode-languageclient/node' +import { CloseAction, ErrorAction, Message } from 'vscode-languageclient/node' +import { formatMessage, toString } from './utils' +import globals from '../../shared/extensionGlobals' +import { getServiceEnvVarConfig } from '../../shared/vscode/env' +import { DevSettings } from '../../shared/settings' +import { + deployTemplateCommand, + rerunValidateAndDeployCommand, + importResourceStateCommand, + cloneResourceStateCommand, + addResourceTypesCommand, + removeResourceTypeCommand, + refreshAllResourcesCommand, + refreshResourceListCommand, + copyResourceIdentifierCommand, + focusDiffCommand, + getStackManagementInfoCommand, + extractToParameterPositionCursorCommand, + loadMoreResourcesCommand, + loadMoreStacksCommand, + searchResourceCommand, + executeChangeSetCommand, + addRelatedResourcesCommand, + refreshChangeSetsCommand, + loadMoreChangeSetsCommand, + viewStackCommand, + createProjectCommand, + addEnvironmentCommand, + removeEnvironmentCommand, + deleteChangeSetCommand, + viewChangeSetCommand, + deployTemplateFromStacksMenuCommand, + selectEnvironmentCommand, +} from './commands/cfnCommands' +import { openStackTemplateCommand } from './commands/openStackTemplate' +import { selectRegionCommand } from './commands/regionCommands' +import { AwsCredentialsService, encryptionKey } from './auth/credentials' +import { ExtensionId, ExtensionName, Version, CloudFormationTelemetrySettings } from './extensionConfig' +import { commandKey } from './utils' +import { CloudFormationExplorer } from './explorer/explorer' +import { handleTelemetryOptIn } from './telemetryOptIn' + +import { refreshCommand, StacksManager } from './stacks/stacksManager' +import { StackOverviewWebviewProvider } from './ui/stackOverviewWebviewProvider' +import { StackEventsWebviewProvider } from './ui/stackEventsWebviewProvider' +import { StackOutputsWebviewProvider } from './ui/stackOutputsWebviewProvider' +import { DiffWebviewProvider } from './ui/diffWebviewProvider' +import { StackResourcesWebviewProvider } from './ui/stackResourcesWebviewProvider' +import { StackViewCoordinator } from './ui/stackViewCoordinator' +import { DocumentManager } from './documents/documentManager' + +import { ResourcesManager } from './resources/resourcesManager' +import { ResourceSelector } from './ui/resourceSelector' +import { RelatedResourcesManager } from './relatedResources/relatedResourcesManager' +import { RelatedResourceSelector } from './ui/relatedResourceSelector' + +import { StackActionCodeLensProvider } from './codelens/stackActionCodeLensProvider' +import { registerStatusBarCommand } from './ui/statusBar' +import { getClientId } from '../../shared/telemetry/util' +import { SettingsLspServerProvider } from './lsp-server/settingsLspServerProvider' +import { DevLspServerProvider } from './lsp-server/devLspServerProvider' +import { RemoteLspServerProvider } from './lsp-server/remoteLspServerProvider' +import { LspServerProvider } from './lsp-server/lspServerProvider' +import { getLogger } from '../../shared/logger/logger' +import { ChangeSetsManager } from './stacks/changeSetsManager' +import { CfnEnvironmentManager } from './cfn-init/cfnEnvironmentManager' +import { CfnEnvironmentSelector } from './ui/cfnEnvironmentSelector' +import { CfnInitUiInterface } from './cfn-init/cfnInitUiInterface' +import { CfnInitCliCaller } from './cfn-init/cfnInitCliCaller' +import { CfnEnvironmentFileSelector } from './ui/cfnEnvironmentFileSelector' +import { fs } from '../../shared/fs/fs' +import { ToolkitError } from '../../shared/errors' + +let client: LanguageClient +let clientDisposables: Disposable[] = [] + +async function startClient(context: ExtensionContext) { + const cfnTelemetrySettings = new CloudFormationTelemetrySettings() + const telemetryEnabled = await handleTelemetryOptIn(context, cfnTelemetrySettings) + + const cfnLspConfig = { + ...DevSettings.instance.getServiceConfig('cloudformationLsp', {}), + ...getServiceEnvVarConfig('cloudformationLsp', ['path', 'cloudformationEndpoint']), + } + + const serverProvider = new LspServerProvider([ + new DevLspServerProvider(context), + new SettingsLspServerProvider(cfnLspConfig), + new RemoteLspServerProvider(), + ]) + const serverFile = await serverProvider.serverExecutable() + if (!(await fs.existsFile(serverFile))) { + throw new Error(`CloudFormation LSP ${serverFile} not found`) + } + getLogger('awsCfnLsp').info(`Found CloudFormation LSP executable: ${serverFile}`) + const serverRootDir = await serverProvider.serverRootDir() + + const envOptions = { + NODE_OPTIONS: '--enable-source-maps', + } + + const serverOptions: ServerOptions = { + run: { + module: serverFile, + transport: TransportKind.ipc, + options: { + env: envOptions, + }, + }, + debug: { + module: serverFile, + transport: TransportKind.ipc, + options: { + execArgv: ['--no-lazy'], + env: envOptions, + }, + }, + } + + const clientOptions: LanguageClientOptions = { + documentSelector: [ + { scheme: 'file', language: 'plaintext' }, + { scheme: 'file', language: 'cloudformation' }, + { scheme: 'file', language: 'template' }, + { scheme: 'file', language: 'json' }, + { scheme: 'file', language: 'yaml' }, + { scheme: 'file', pattern: '**/*.txt' }, + { scheme: 'file', pattern: '**/*.template' }, + { scheme: 'file', pattern: '**/*.cfn' }, + { scheme: 'file', pattern: '**/*.json' }, + { scheme: 'file', pattern: '**/*.yaml' }, + ], + initializationOptions: { + handledSchemaProtocols: ['file'], + aws: { + clientInfo: { + extension: { + name: ExtensionId, + version: Version, + }, + clientId: getClientId(globals.globalState, telemetryEnabled), + }, + telemetryEnabled: telemetryEnabled, + ...(cfnLspConfig.cloudformationEndpoint && { + cloudformation: { + endpoint: cfnLspConfig.cloudformationEndpoint, + }, + }), + encryption: { + key: encryptionKey.toString('base64'), + mode: 'JWT', + }, + }, + }, + errorHandler: { + error: (error: Error, message: Message | undefined, count: number | undefined): ErrorHandlerResult => { + void window.showErrorMessage(formatMessage(`${toString(message)} - ${toString(error)}`)) + return { action: ErrorAction.Continue } + }, + closed: (): CloseHandlerResult => { + return { action: CloseAction.DoNotRestart } + }, + }, + } + + client = new LanguageClient(ExtensionId, ExtensionName, serverOptions, clientOptions) + + const stacksManager = new StacksManager(client) + + await client.start() + + const documentManager = new DocumentManager(client) + const resourceSelector = new ResourceSelector(client) + const resourcesManager = new ResourcesManager(client, resourceSelector) + const relatedResourceSelector = new RelatedResourceSelector(client) + const relatedResourcesManager = new RelatedResourcesManager( + client, + relatedResourceSelector, + resourceSelector, + resourcesManager + ) + const changeSetManager = new ChangeSetsManager(client) + const environmentSelector = new CfnEnvironmentSelector() + const environmentFileSelector = new CfnEnvironmentFileSelector() + const environmentManager = new CfnEnvironmentManager(client, environmentSelector, environmentFileSelector) + + const cfnInitCliCaller = new CfnInitCliCaller(serverRootDir) + const cfnInitUiInterface = new CfnInitUiInterface(cfnInitCliCaller) + + const cfnExplorer = new CloudFormationExplorer( + stacksManager, + resourcesManager, + changeSetManager, + documentManager, + globals.regionProvider, + environmentManager + ) + + resourceSelector.setRefreshCallback(() => cfnExplorer.refresh()) + + resourcesManager.addListener(() => { + cfnExplorer.refresh() + }) + stacksManager.addListener(() => { + cfnExplorer.refresh() + }) + documentManager.addListener(() => { + cfnExplorer.refresh() + }) + environmentManager.addListener(() => { + cfnExplorer.refresh() + }) + + const credentialsService = new AwsCredentialsService(stacksManager, resourcesManager, cfnExplorer.regionManager) + cfnExplorer.setCredentialsService(credentialsService) + + const stackViewCoordinator = new StackViewCoordinator() + stackViewCoordinator.setStackStatusUpdateCallback((stackName, stackStatus) => { + stacksManager.updateStackStatus(stackName, stackStatus) + cfnExplorer.refresh() + }) + + const diffProvider = new DiffWebviewProvider(stackViewCoordinator) + const resourcesProvider = new StackResourcesWebviewProvider(client, stackViewCoordinator) + const overviewProvider = new StackOverviewWebviewProvider(client, stackViewCoordinator) + const eventsProvider = new StackEventsWebviewProvider(client, stackViewCoordinator) + const outputsProvider = new StackOutputsWebviewProvider(client, stackViewCoordinator) + + const documentSelector = [ + { scheme: 'file', language: 'cloudformation' }, + { scheme: 'file', language: 'yaml' }, + { scheme: 'file', language: 'json' }, + ] + + const codeLensProvider = languages.registerCodeLensProvider( + documentSelector, + new StackActionCodeLensProvider(client) + ) + + clientDisposables = [ + codeLensProvider, + stacksManager, + window.createTreeView('aws.cloudformation', { + treeDataProvider: cfnExplorer, + showCollapseAll: true, + canSelectMany: true, + }), + loadMoreResourcesCommand(cfnExplorer), + loadMoreStacksCommand(cfnExplorer), + searchResourceCommand(cfnExplorer, resourcesManager), + refreshChangeSetsCommand(cfnExplorer), + loadMoreChangeSetsCommand(cfnExplorer), + viewStackCommand(stackViewCoordinator, overviewProvider, outputsProvider, resourcesProvider), + addResourceTypesCommand(resourcesManager), + removeResourceTypeCommand(resourcesManager), + refreshAllResourcesCommand(resourcesManager), + refreshResourceListCommand(resourcesManager, cfnExplorer), + copyResourceIdentifierCommand(), + importResourceStateCommand(resourcesManager), + cloneResourceStateCommand(resourcesManager), + getStackManagementInfoCommand(resourcesManager), + window.registerWebviewViewProvider(commandKey('stack.overview'), overviewProvider), + window.registerWebviewViewProvider(commandKey('diff'), diffProvider), + window.registerWebviewViewProvider(commandKey('stack.events'), eventsProvider), + window.registerWebviewViewProvider(commandKey('stack.resources'), resourcesProvider), + window.registerWebviewViewProvider(commandKey('stack.outputs'), outputsProvider), + focusDiffCommand(), + deployTemplateCommand(client, diffProvider, documentManager, environmentManager), + deployTemplateFromStacksMenuCommand(), + executeChangeSetCommand(client, stackViewCoordinator), + deleteChangeSetCommand(client), + viewChangeSetCommand(client, diffProvider), + refreshCommand(stacksManager), + openStackTemplateCommand(client), + selectRegionCommand(cfnExplorer), + selectEnvironmentCommand(cfnExplorer), + rerunValidateAndDeployCommand(), + extractToParameterPositionCursorCommand(client), + createProjectCommand(cfnInitUiInterface), + addEnvironmentCommand(cfnInitUiInterface, cfnInitCliCaller, environmentManager), + removeEnvironmentCommand(cfnInitCliCaller, environmentManager), + addRelatedResourcesCommand(relatedResourcesManager), + credentialsService, + serverProvider, + { dispose: () => client?.stop() }, + ] + + registerStatusBarCommand() + + context.subscriptions.push(...clientDisposables) + await credentialsService.initialize(client) +} + +async function restartClient(context: ExtensionContext) { + // Dispose all client-related resources + for (const disposable of clientDisposables) { + disposable.dispose() + } + clientDisposables = [] + + // Start new client + await startClient(context) +} + +export async function activate(context: ExtensionContext) { + context.subscriptions.push( + commands.registerCommand(commandKey('server.restartServer'), async () => { + try { + await restartClient(context) + } catch (error) { + void window.showErrorMessage( + formatMessage(`Failed to restart CloudFormation language server: ${toString(error)}`) + ) + } + }) + ) + + try { + await startClient(context) + } catch (err) { + getLogger('awsCfnLsp').error(ToolkitError.chain(err, 'CloudFormation language server failed to start')) + } +} + +export function deactivate(): Thenable | undefined { + if (!client) { + return undefined + } + + return client.stop() +} diff --git a/packages/core/src/awsService/cloudformation/extensionConfig.ts b/packages/core/src/awsService/cloudformation/extensionConfig.ts new file mode 100644 index 00000000000..c3577d9c19b --- /dev/null +++ b/packages/core/src/awsService/cloudformation/extensionConfig.ts @@ -0,0 +1,15 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fromExtensionManifest } from '../../shared/settings' + +export const ExtensionId = 'amazonwebservices.cloudformation' +export const ExtensionName = 'AWS CloudFormation' +export const Version = '1.0.0' +export const ExtensionConfigKey = 'aws.cloudformation' + +export class CloudFormationTelemetrySettings extends fromExtensionManifest(`${ExtensionConfigKey}.telemetry`, { + enabled: Boolean, +}) {} diff --git a/packages/core/src/awsService/cloudformation/lsp-server/CLibCheck.ts b/packages/core/src/awsService/cloudformation/lsp-server/CLibCheck.ts new file mode 100644 index 00000000000..5a21ca9b799 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/lsp-server/CLibCheck.ts @@ -0,0 +1,141 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { execSync } from 'child_process' // eslint-disable-line no-restricted-imports, aws-toolkits/no-string-exec-for-child-process +import * as fs from 'fs' // eslint-disable-line no-restricted-imports +import * as semver from 'semver' +import { getLogger } from '../../../shared/logger/logger' + +interface VersionResult { + maxFound: string | undefined + allAvailable: string[] +} + +export class CLibCheck { + /** + * Checks the GNU C Library (glibc) version. + * Uses `ldd --version` to parse the version number. + */ + public static getGLibCVersion(): string | undefined { + try { + const output = execSync('ldd --version', { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + timeout: 5000, + }) + // Output usually looks like: "ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35" + // We look for the first version number pattern on the first line. + const firstLine = output.split('\n')[0] + const match = firstLine.match(/(\d+\.\d+)/) + return match ? semver.coerce(match[0])?.version || match[0] : undefined + } catch (error) { + getLogger('awsCfnLsp').warn('Could not run ldd. Is this a glibc-based distro?') + return undefined + } + } + + /** + * Checks available GLIBCXX versions in libstdc++. + * 1. Finds libstdc++.so.6 location. + * 2. Scans the binary for "GLIBCXX_*" strings. + * 3. Sorts them to find the maximum version supported. + */ + public static getGLibCXXVersions(): VersionResult { + const libPath = this.findLibStdCpp() + + if (!libPath) { + return { maxFound: undefined, allAvailable: [] } + } + + try { + // Method 1: Try using the `strings` command (fastest, but requires binutils) + const output = execSync(`strings "${libPath}" | grep GLIBCXX`, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + timeout: 10000, + }) + return this.parseGLibCXXOutput(output) + } catch (e) { + // Method 2: Fallback to Node.js FS reading (works in minimal containers w/o strings) + try { + const content = fs.readFileSync(libPath, 'binary') // Read as binary string + // Regex to find all GLIBCXX_x.x.x occurrences + const matches = content.match(/GLIBCXX_\d+\.\d+(\.\d+)?/g) + if (matches) { + return this.parseGLibCXXOutput(matches.join('\n')) + } + } catch (readError) { + getLogger('awsCfnLsp').error(`Failed to read library at ${libPath}`) + } + } + + return { maxFound: undefined, allAvailable: [] } + } + + private static parseGLibCXXOutput(rawOutput: string): VersionResult { + const rawVersions = rawOutput + .trim() + .split('\n') + .map((line) => line.trim()) + // 1. Strict Filter: Must be GLIBCXX_ followed immediately by a digit + .filter((line) => /^GLIBCXX_\d/.test(line)) + // 2. Extraction: Capture strictly the numeric part + .map((line) => { + const match = line.match(/^GLIBCXX_(\d+\.\d+(?:\.\d+)?)/) + return match ? match[1] : undefined + }) + .filter((v): v is string => v !== undefined) + + // 3. Deduplicate + const uniqueVersions = [...new Set(rawVersions)] + + // 4. Sort using Semver + // We use coerce() because "3.4" is not valid strict semver, but "3.4.0" is. + const sorted = uniqueVersions.sort((a, b) => { + const verA = semver.coerce(a) + const verB = semver.coerce(b) + // Handle unlikely case where coerce fails (returns null) by pushing it to the bottom + if (!verA || !verB) { + return 0 + } + return semver.compare(verA, verB) + }) + + return { + maxFound: sorted.length > 0 ? sorted[sorted.length - 1] : undefined, + allAvailable: sorted, + } + } + + private static findLibStdCpp(): string | undefined { + // 1. Try ldconfig cache (most reliable on standard linux) + try { + const ldconfig = execSync('/sbin/ldconfig -p | grep libstdc++.so.6', { encoding: 'utf8', timeout: 5000 }) + // Output: "libstdc++.so.6 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstdc++.so.6" + const match = ldconfig.match(/=>\s+(.+)$/m) + if (match && match[1]) { + return match[1].trim() + } + } catch (e) { + /* ignore */ + } + + // 2. Search common paths (fallback for containers/weird setups) + const commonPaths = [ + '/usr/lib/x86_64-linux-gnu/libstdc++.so.6', + '/usr/lib64/libstdc++.so.6', + '/usr/lib/libstdc++.so.6', + '/lib/x86_64-linux-gnu/libstdc++.so.6', + ] + + for (const p of commonPaths) { + if (fs.existsSync(p)) { + return p + } + } + + return undefined + } +} diff --git a/packages/core/src/awsService/cloudformation/lsp-server/devLspServerProvider.ts b/packages/core/src/awsService/cloudformation/lsp-server/devLspServerProvider.ts new file mode 100644 index 00000000000..604f4ac132c --- /dev/null +++ b/packages/core/src/awsService/cloudformation/lsp-server/devLspServerProvider.ts @@ -0,0 +1,71 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { dirname, join } from 'path' +import { ExtensionContext } from 'vscode' +import { LspServerProviderI } from './lspServerProvider' +import { CfnLspServerFile } from './lspServerConfig' +import { existsSync, readdirSync } from 'fs' // eslint-disable-line no-restricted-imports +import { isDebugInstance } from '../../../shared/vscode/env' +import { getLogger } from '../../../shared/logger/logger' + +export class DevLspServerProvider implements LspServerProviderI { + private readonly devServerLocation?: string + + constructor(context: ExtensionContext) { + this.devServerLocation = findServerInDevelopment(context.extensionPath) + } + + name(): string { + return 'DevLspServerProvider' + } + + canProvide(): boolean { + return isDebugInstance() && this.devServerLocation !== undefined + } + + async serverExecutable(): Promise { + return Promise.resolve(this.devServerLocation!) + } + + async serverRootDir(): Promise { + return Promise.resolve(dirname(this.devServerLocation!)) + } +} + +function findServerInDevelopment(path: string): string | undefined { + const parentDir = dirname(dirname(dirname(path))) + const possibleLocations = [] + + // Get all directories in parent directory + const siblingDirs = readdirSync(parentDir, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name) + + // Check each sibling directory for bundle/development structure + for (const siblingDir of siblingDirs) { + const serverPath = join(parentDir, siblingDir, 'bundle', 'development', CfnLspServerFile) + if (existsSync(serverPath)) { + possibleLocations.push(serverPath) + } + } + + const validLocations = possibleLocations.filter((path) => { + return existsSync(path) + }) + + if (validLocations.length < 1) { + return undefined + } + + if (validLocations.length === 1) { + getLogger().debug(`Found CloudFormation LSP dev server ${possibleLocations[0]}`) + return possibleLocations[0] + } + + throw Error( + `Found ${validLocations.length} locations with server executable file: ${JSON.stringify(possibleLocations)}` + ) +} diff --git a/packages/core/src/awsService/cloudformation/lsp-server/githubManifestAdapter.ts b/packages/core/src/awsService/cloudformation/lsp-server/githubManifestAdapter.ts new file mode 100644 index 00000000000..88dfeb441c2 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/lsp-server/githubManifestAdapter.ts @@ -0,0 +1,201 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CfnLspName, CfnLspServerEnvType } from './lspServerConfig' +import { + addWindows, + CfnManifest, + CfnTarget, + CfnLspVersion, + dedupeAndGetLatestVersions, + extractPlatformAndArch, + useOldLinuxVersion, + mapLegacyLinux, +} from './utils' +import { getLogger } from '../../../shared/logger/logger' +import { ToolkitError } from '../../../shared/errors' + +export class GitHubManifestAdapter { + constructor( + private readonly repoOwner: string, + private readonly repoName: string, + readonly environment: CfnLspServerEnvType + ) {} + + async getManifest(): Promise { + let manifest: CfnManifest + try { + manifest = await this.getManifestJson() + } catch (err) { + getLogger('awsCfnLsp').error(ToolkitError.chain(err, 'Failed to get CloudFormation manifest')) + manifest = await this.getFromReleases() + } + + getLogger('awsCfnLsp').info( + 'Candidate versions: %s', + manifest.versions + .map( + (v) => + `${v.serverVersion}[${v.targets + .sort() + .map((t) => `${t.platform}-${t.arch}-${t.nodejs}`) + .join(',')}]` + ) + .join(', ') + ) + + if (process.platform !== 'linux') { + return manifest + } + + const useFallbackLinux = useOldLinuxVersion() + if (!useFallbackLinux) { + return manifest + } + + getLogger('awsCfnLsp').info('In a legacy or sandbox Linux environment') + manifest.versions = mapLegacyLinux(manifest.versions) + + getLogger('awsCfnLsp').info( + 'Remapped candidate versions: %s', + manifest.versions + .map( + (v) => + `${v.serverVersion}[${v.targets + .sort() + .map((t) => `${t.platform}-${t.arch}-${t.nodejs}`) + .join(',')}]` + ) + .join(', ') + ) + return manifest + } + + private async getFromReleases(): Promise { + const releases = await this.fetchGitHubReleases() + const envReleases = this.filterByEnvironment(releases) + const sortedReleases = envReleases.sort((a, b) => { + return b.tag_name.localeCompare(a.tag_name) + }) + const versions = dedupeAndGetLatestVersions(sortedReleases.map((release) => this.convertRelease(release))) + getLogger('awsCfnLsp').info( + 'Candidate versions: %s', + versions + .map((v) => `${v.serverVersion}[${v.targets.map((t) => `${t.platform}-${t.arch}`).join(',')}]`) + .join(', ') + ) + return { + manifestSchemaVersion: '1.0', + artifactId: CfnLspName, + artifactDescription: 'GitHub CloudFormation Language Server', + isManifestDeprecated: false, + versions: versions, + } + } + + private filterByEnvironment(releases: GitHubRelease[]): GitHubRelease[] { + return releases.filter((release) => { + const tag = release.tag_name + if (this.environment === 'alpha') { + return release.prerelease && tag.endsWith('-alpha') + } else if (this.environment === 'beta') { + return release.prerelease && tag.endsWith('-beta') + } else { + return !release.prerelease + } + }) + } + + private async fetchGitHubReleases(): Promise { + const response = await fetch(`https://api.github.com/repos/${this.repoOwner}/${this.repoName}/releases`) + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status}`) + } + return response.json() + } + + private convertRelease(release: GitHubRelease): CfnLspVersion { + return { + serverVersion: release.tag_name, + isDelisted: false, + targets: addWindows(this.extractTargets(release.assets)), + } + } + + private extractTargets(assets: GitHubAsset[]): CfnTarget[] { + return assets.map((asset) => { + const { arch, platform, nodejs } = extractPlatformAndArch(asset.name) + + return { + platform, + arch, + nodejs, + contents: [ + { + filename: asset.name, + url: asset.browser_download_url, + hashes: [], + bytes: asset.size, + }, + ], + } + }) + } + + private async getManifestJson(): Promise { + const response = await fetch( + `https://raw.githubusercontent.com/${this.repoOwner}/${this.repoName}/refs/heads/main/assets/release-manifest.json` + ) + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status}`) + } + + const json = (await response.json()) as Record + + return { + manifestSchemaVersion: json.manifestSchemaVersion as string, + artifactId: json.artifactId as string, + artifactDescription: json.artifactDescription as string, + isManifestDeprecated: json.isManifestDeprecated as boolean, + versions: json[this.environment] as CfnLspVersion[], + } + } +} + +/* eslint-disable @typescript-eslint/naming-convention */ +interface GitHubAsset { + url: string + browser_download_url: string + id: number + node_id: string + name: string + label: string | null + state: string + content_type: string + size: number + download_count: number + created_at: string + updated_at: string +} + +interface GitHubRelease { + url: string + html_url: string + assets_url: string + upload_url: string + tarball_url: string | null + zipball_url: string | null + id: number + node_id: string + tag_name: string + target_commitish: string + name: string | null + body: string | null + draft: boolean + prerelease: boolean + created_at: string // ISO 8601 date string + published_at: string | null // ISO 8601 date string + assets: GitHubAsset[] +} diff --git a/packages/core/src/awsService/cloudformation/lsp-server/lspInstaller.ts b/packages/core/src/awsService/cloudformation/lsp-server/lspInstaller.ts new file mode 100644 index 00000000000..2d9c811af55 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/lsp-server/lspInstaller.ts @@ -0,0 +1,107 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BaseLspInstaller } from '../../../shared/lsp/baseLspInstaller' +import { GitHubManifestAdapter } from './githubManifestAdapter' +import { fs } from '../../../shared/fs/fs' +import { CfnLspName, CfnLspServerEnvType, CfnLspServerFile } from './lspServerConfig' +import { isAutomation, isBeta, isDebugInstance } from '../../../shared/vscode/env' +import { dirname, join } from 'path' +import { getLogger } from '../../../shared/logger/logger' +import { ResourcePaths } from '../../../shared/lsp/types' +import * as nodeFs from 'fs' // eslint-disable-line no-restricted-imports +import globals from '../../../shared/extensionGlobals' +import { toString } from '../utils' + +function determineEnvironment(): CfnLspServerEnvType { + if (isDebugInstance()) { + return 'alpha' + } else if (isBeta() || isAutomation()) { + return 'beta' + } + return 'prod' +} + +export class CfnLspInstaller extends BaseLspInstaller { + private readonly githubManifest = new GitHubManifestAdapter( + 'aws-cloudformation', + 'cloudformation-languageserver', + determineEnvironment() + ) + + constructor() { + super( + { + manifestUrl: 'github', + supportedVersions: '<2.0.0', + id: CfnLspName, + suppressPromptPrefix: 'cfnLsp', + }, + 'awsCfnLsp', + { + resolve: async () => { + const log = getLogger('awsCfnLsp') + const cfnManifestStorageKey = 'aws.cloudformation.lsp.manifest' + + try { + const manifest = await this.githubManifest.getManifest() + log.info( + `Creating CloudFormation LSP manifest for ${this.githubManifest.environment}`, + manifest.versions.map((v) => v.serverVersion) + ) + + // Cache in CloudFormation-specific global state storage + globals.globalState.tryUpdate(cfnManifestStorageKey, { + content: JSON.stringify(manifest), + }) + + return manifest + } catch (error) { + log.warn(`GitHub fetch failed, trying cached manifest: ${error}`) + + // Try cached manifest from CloudFormation-specific storage + const manifestData = globals.globalState.tryGet(cfnManifestStorageKey, Object, {}) + + if (manifestData?.content) { + log.debug('Using cached manifest for offline mode') + return JSON.parse(manifestData.content) + } + + log.error('No cached manifest found') + throw error + } + }, + } as any + ) + } + + protected async postInstall(assetDirectory: string): Promise { + const resourcePaths = this.resourcePaths(assetDirectory) + const rootDir = dirname(resourcePaths.lsp) + await fs.chmod(join(rootDir, 'bin', process.platform === 'win32' ? 'cfn-init.exe' : 'cfn-init'), 0o755) + } + + protected resourcePaths(assetDirectory?: string): ResourcePaths { + if (!assetDirectory) { + return { + lsp: this.config.path ?? CfnLspServerFile, + node: process.execPath, + } + } + + // Find the single extracted directory + const entries = nodeFs.readdirSync(assetDirectory, { withFileTypes: true }) + const folders = entries.filter((entry) => entry.isDirectory()) + + if (folders.length !== 1) { + throw new Error(`${folders.length} CloudFormation LSP folders found ${toString(folders)}`) + } + + return { + lsp: join(assetDirectory, folders[0].name, CfnLspServerFile), + node: process.execPath, + } + } +} diff --git a/packages/core/src/awsService/cloudformation/lsp-server/lspServerConfig.ts b/packages/core/src/awsService/cloudformation/lsp-server/lspServerConfig.ts new file mode 100644 index 00000000000..afc8cfd7ede --- /dev/null +++ b/packages/core/src/awsService/cloudformation/lsp-server/lspServerConfig.ts @@ -0,0 +1,17 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const CfnLspName = 'cloudformation-languageserver' +export const CfnLspServerFile = 'cfn-lsp-server-standalone.js' +export const CfnLspServerStorageName = '.aws-cfn-storage' +export const RequiredFiles = [ + 'node_modules', + 'cfn-lsp-server-standalone.js', + 'package.json', + 'pyodide-worker.js', + 'assets', +] + +export type CfnLspServerEnvType = 'alpha' | 'beta' | 'prod' diff --git a/packages/core/src/awsService/cloudformation/lsp-server/lspServerProvider.ts b/packages/core/src/awsService/cloudformation/lsp-server/lspServerProvider.ts new file mode 100644 index 00000000000..3ecd11208aa --- /dev/null +++ b/packages/core/src/awsService/cloudformation/lsp-server/lspServerProvider.ts @@ -0,0 +1,70 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Disposable } from 'vscode' +import { getLogger } from '../../../shared/logger/logger' +import { ToolkitError } from '../../../shared/errors' + +export interface LspServerResolverI { + serverExecutable(): Promise + serverRootDir(): Promise +} + +export interface LspServerProviderI extends LspServerResolverI { + canProvide(): boolean + name(): string +} + +export class LspServerProvider implements LspServerResolverI, Disposable { + private readonly matchedProviders: LspServerProviderI[] + private _serverExecutable?: string + private _serverRootDir?: string + + constructor(providers: LspServerProviderI[]) { + const matches = providers.filter((provider) => provider.canProvide()) + + if (matches.length < 1) { + throw new Error(`Matched with 0 CloudFormation LSP providers`) + } + + this.matchedProviders = matches + getLogger('awsCfnLsp').info( + `Found CloudFormation LSP provider: ${this.matchedProviders.map((provider) => provider.name())}` + ) + } + + async serverExecutable(): Promise { + await this.evaluateProviders() + return this._serverExecutable! + } + + async serverRootDir(): Promise { + await this.evaluateProviders() + return this._serverRootDir! + } + + private async evaluateProviders() { + if (this._serverExecutable && this._serverRootDir) { + return + } + + for (const provider of this.matchedProviders) { + try { + const executable = await provider.serverExecutable() + const dir = await provider.serverRootDir() + + this._serverExecutable = executable + this._serverRootDir = dir + return + } catch (err) { + getLogger('awsCfnLsp').error( + ToolkitError.chain(err, `Failed to resolve CloudFormation LSP provider ${provider.name()}`) + ) + } + } + } + + dispose() {} +} diff --git a/packages/core/src/awsService/cloudformation/lsp-server/remoteLspServerProvider.ts b/packages/core/src/awsService/cloudformation/lsp-server/remoteLspServerProvider.ts new file mode 100644 index 00000000000..19ef80724dc --- /dev/null +++ b/packages/core/src/awsService/cloudformation/lsp-server/remoteLspServerProvider.ts @@ -0,0 +1,35 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { dirname } from 'path' +import { LspServerProviderI } from './lspServerProvider' +import { CfnLspInstaller } from './lspInstaller' + +export class RemoteLspServerProvider implements LspServerProviderI { + private installer = new CfnLspInstaller() + private serverPath?: string + + name(): string { + return 'RemoteLspServerProvider' + } + + canProvide(): boolean { + return true + } + + async serverExecutable(): Promise { + if (this.serverPath) { + return this.serverPath + } + + const result = await this.installer.resolve() + this.serverPath = result.resourcePaths.lsp + return this.serverPath + } + + async serverRootDir(): Promise { + return dirname(await this.serverExecutable()) + } +} diff --git a/packages/core/src/awsService/cloudformation/lsp-server/settingsLspServerProvider.ts b/packages/core/src/awsService/cloudformation/lsp-server/settingsLspServerProvider.ts new file mode 100644 index 00000000000..8d08bb9bcc6 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/lsp-server/settingsLspServerProvider.ts @@ -0,0 +1,33 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { dirname, join } from 'path' +import { LspServerProviderI } from './lspServerProvider' +import { CfnLspServerFile } from './lspServerConfig' + +export class SettingsLspServerProvider implements LspServerProviderI { + private readonly path?: string + + constructor(config?: { path?: string }) { + this.path = config?.path + } + + name(): string { + return 'SettingsLspServerProvider' + } + + canProvide(): boolean { + return this.path !== undefined + } + + async serverExecutable(): Promise { + const serverFile = join(this.path!, CfnLspServerFile) + return Promise.resolve(serverFile) + } + + async serverRootDir(): Promise { + return Promise.resolve(dirname(await this.serverExecutable())) + } +} diff --git a/packages/core/src/awsService/cloudformation/lsp-server/utils.ts b/packages/core/src/awsService/cloudformation/lsp-server/utils.ts new file mode 100644 index 00000000000..9775fb0036b --- /dev/null +++ b/packages/core/src/awsService/cloudformation/lsp-server/utils.ts @@ -0,0 +1,175 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LspVersion, Target, Manifest } from '../../../shared/lsp/types' +import * as semver from 'semver' +import { CLibCheck } from './CLibCheck' +import { toString } from '../utils' +import { getLogger } from '../../../shared/logger/logger' + +export interface CfnTarget extends Target { + nodejs?: string +} +export interface CfnLspVersion extends LspVersion { + targets: CfnTarget[] +} +export interface CfnManifest extends Manifest { + versions: CfnLspVersion[] +} + +export function addWindows(targets: CfnTarget[]): CfnTarget[] { + const win32Targets = targets.filter((target) => { + return target.platform === 'win32' + }) + + const windowsTargets = targets.filter((target) => { + return target.platform === 'windows' + }) + + if (win32Targets.length < 1 || windowsTargets.length > 0) { + return targets + } + + return [ + ...targets, + ...win32Targets.map((target) => { + return { + ...target, + platform: 'windows', + } + }), + ] +} + +export function dedupeAndGetLatestVersions(versions: CfnLspVersion[]): CfnLspVersion[] { + const grouped: Record = {} + + // Group by normalized version + for (const version of versions) { + const normalizedV = getMajorMinorPatchVersion(version.serverVersion) + if (!grouped[normalizedV]) { + grouped[normalizedV] = [] + } + grouped[normalizedV].push(version) + } + + const groupedAndSorted: Record = Object.fromEntries( + Object.entries(grouped).sort(([v1], [v2]) => { + return compareVersionsDesc(v1, v2) + }) + ) + + // Sort each group by version descending and pick the first (latest) + return Object.values(groupedAndSorted).map((group) => { + group.sort((a, b) => compareVersionsDesc(a.serverVersion, b.serverVersion)) + const latest = group[0] + latest.serverVersion = `${latest.serverVersion.replace('v', '')}` + + return latest // take the highest version + }) +} + +function compareVersionsDesc(v1: string, v2: string) { + const a = convertVersionToNumbers(v1) + const b = convertVersionToNumbers(v2) + + for (let i = 0; i < Math.max(a.length, b.length); i++) { + const partA = a[i] || 0 + const partB = b[i] || 0 + + if (partA > partB) { + return -1 + } + if (partA < partB) { + return 1 + } + } + return 0 +} + +function removeWordsFromVersion(version: string): string { + return version.replaceAll('-beta', '').replaceAll('-alpha', '').replaceAll('-prod', '').replaceAll('v', '') +} + +function convertVersionToNumbers(version: string): number[] { + return removeWordsFromVersion(version).replaceAll('-', '.').split('.').map(Number) +} + +function getMajorMinorPatchVersion(version: string): string { + return removeWordsFromVersion(version).split('-')[0] +} + +export function extractPlatformAndArch(filename: string): { platform: string; arch: string; nodejs?: string } { + const match = filename.match(/^cloudformation-languageserver-(.*)-(.*)-(x64|arm64)(?:-node(\d+))?\.zip$/) + if (match === null) { + throw new Error(`Could not extract platform from ${filename}`) + } + + const platform = match[2] + const arch = match[3] + const nodejs = match[4] + + if (!platform || !arch) { + throw new Error(`Unknown arch and platform ${arch} ${platform}`) + } + + return { arch, platform, nodejs } +} + +export function useOldLinuxVersion(): boolean { + if (process.platform !== 'linux') { + return false + } + + if (process.env.SNAP !== undefined) { + return true + } + + const glibcxx = CLibCheck.getGLibCXXVersions() + const maxAvailGLibCXX = glibcxx.maxFound + if (!maxAvailGLibCXX) { + return false + } + + getLogger('awsCfnLsp').info(`Found GLIBCXX ${toString(glibcxx)}`) + return semver.lt(maxAvailGLibCXX, '3.4.29') +} + +const LegacyLinuxGLibPlatform = 'linuxglib2.28' + +export function mapLegacyLinux(versions: CfnLspVersion[]): CfnLspVersion[] { + const remappedVersions: CfnLspVersion[] = [] + + for (const version of versions) { + const hasLegacyLinux = version.targets.some((t) => t.platform === LegacyLinuxGLibPlatform) + + if (!hasLegacyLinux) { + getLogger('awsCfnLsp').warn(`Found no compatible legacy linux builds for ${version.serverVersion}`) + remappedVersions.push(version) + } else { + const newTargets = version.targets + .filter((target) => { + return target.platform !== 'linux' + }) + .map((target) => { + if (target.platform !== LegacyLinuxGLibPlatform) { + return target + } + + return { + ...target, + platform: 'linux', + } + }) + + remappedVersions.push({ + ...version, + targets: newTargets, + }) + } + } + + return remappedVersions +} diff --git a/packages/core/src/awsService/cloudformation/lspTypes.ts b/packages/core/src/awsService/cloudformation/lspTypes.ts new file mode 100644 index 00000000000..602587203cb --- /dev/null +++ b/packages/core/src/awsService/cloudformation/lspTypes.ts @@ -0,0 +1,8 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export type Identifiable = { + id: string +} diff --git a/packages/core/src/awsService/cloudformation/relatedResources/relatedResourcesApi.ts b/packages/core/src/awsService/cloudformation/relatedResources/relatedResourcesApi.ts new file mode 100644 index 00000000000..95592e0a9c2 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/relatedResources/relatedResourcesApi.ts @@ -0,0 +1,33 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LanguageClient } from 'vscode-languageclient/node' +import { + GetAuthoredResourceTypesRequest, + GetRelatedResourceTypesParams, + GetRelatedResourceTypesRequest, + InsertRelatedResourcesParams, + InsertRelatedResourcesRequest, + RelatedResourcesCodeAction, + TemplateUri, +} from './relatedResourcesProtocol' + +export async function getAuthoredResourceTypes(client: LanguageClient, templateUri: TemplateUri): Promise { + return client.sendRequest(GetAuthoredResourceTypesRequest, templateUri) +} + +export async function getRelatedResourceTypes( + client: LanguageClient, + params: GetRelatedResourceTypesParams +): Promise { + return client.sendRequest(GetRelatedResourceTypesRequest, params) +} + +export async function insertRelatedResources( + client: LanguageClient, + params: InsertRelatedResourcesParams +): Promise { + return client.sendRequest(InsertRelatedResourcesRequest, params) +} diff --git a/packages/core/src/awsService/cloudformation/relatedResources/relatedResourcesManager.ts b/packages/core/src/awsService/cloudformation/relatedResources/relatedResourcesManager.ts new file mode 100644 index 00000000000..78b151de333 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/relatedResources/relatedResourcesManager.ts @@ -0,0 +1,128 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Position, Range, TextEdit, TextEditorRevealType, Uri, window, workspace, WorkspaceEdit } from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node' +import { RelatedResourceSelector } from '../ui/relatedResourceSelector' +import { ResourceSelector } from '../ui/resourceSelector' +import { insertRelatedResources } from './relatedResourcesApi' +import { RelatedResourcesCodeAction } from './relatedResourcesProtocol' +import { showErrorMessage } from '../ui/message' +import { ResourceNode } from '../explorer/nodes/resourceNode' +import { ResourcesManager } from '../resources/resourcesManager' + +export class RelatedResourcesManager { + constructor( + private client: LanguageClient, + private selector: RelatedResourceSelector, + private resourceSelector: ResourceSelector, + private resourcesManager: ResourcesManager + ) {} + + async addRelatedResources(preSelectedResourceType?: string): Promise { + const activeEditor = window.activeTextEditor + if (!activeEditor) { + void window.showErrorMessage('No template file opened') + return + } + + try { + const templateUri = activeEditor.document.uri.toString() + + const selectedParentResourceType = + preSelectedResourceType || (await this.selector.selectAuthoredResourceType(templateUri)) + if (!selectedParentResourceType) { + return + } + + const selectedRelatedTypes = await this.selector.selectRelatedResourceTypes(selectedParentResourceType) + if (!selectedRelatedTypes || selectedRelatedTypes.length === 0) { + return + } + + const action = await this.selector.promptCreateOrImport() + if (!action) { + return + } + + if (action === 'create') { + await this.createRelatedResources(templateUri, selectedParentResourceType, selectedRelatedTypes) + } else { + await this.importRelatedResources(selectedRelatedTypes, selectedParentResourceType) + } + } catch (error) { + showErrorMessage( + `Error adding related resources: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + private async createRelatedResources( + templateUri: string, + parentResourceType: string, + relatedResourceTypes: string[] + ): Promise { + const result = await insertRelatedResources(this.client, { + templateUri, + relatedResourceTypes, + parentResourceType, + }) + + await this.applyCodeAction(result) + + const activeEditor = window.activeTextEditor + if (activeEditor && result.data?.scrollToPosition) { + const position = new Position(result.data.scrollToPosition.line, result.data.scrollToPosition.character) + const revealRange = new Range( + new Position(Math.max(0, position.line - 2), 0), + new Position(position.line + 8, 0) + ) + activeEditor.revealRange(revealRange, TextEditorRevealType.InCenter) + } + + void window.showInformationMessage(`Added ${relatedResourceTypes.length} related resources`) + } + + private async applyCodeAction(codeAction: RelatedResourcesCodeAction): Promise { + if (codeAction.edit?.changes) { + const workspaceEdit = new WorkspaceEdit() + + for (const [uri, textEdits] of Object.entries(codeAction.edit.changes)) { + const docUri = Uri.parse(uri) + const docEdits = textEdits.map((edit) => { + const range = new Range( + new Position(edit.range.start.line, edit.range.start.character), + new Position(edit.range.end.line, edit.range.end.character) + ) + return new TextEdit(range, edit.newText) + }) + workspaceEdit.set(docUri, docEdits) + } + + await workspace.applyEdit(workspaceEdit) + } + } + + private async importRelatedResources( + relatedResourceTypes: string[], + selectedParentResourceType: string + ): Promise { + const selections = await this.resourceSelector.selectResources(true, relatedResourceTypes, { + getCached: (type) => this.resourcesManager.get().find((r) => r.typeName === type), + loadMore: (type, token) => this.resourcesManager.loadMoreResources(type, token), + search: (type, id) => this.resourcesManager.searchResource(type, id), + }) + if (selections.length === 0) { + return + } + + const resourceNodes = selections.map((selection) => ({ + resourceType: selection.resourceType, + resourceIdentifier: selection.resourceIdentifier, + })) as ResourceNode[] + + await this.resourcesManager.importResourceStates(resourceNodes, selectedParentResourceType) + } +} diff --git a/packages/core/src/awsService/cloudformation/relatedResources/relatedResourcesProtocol.ts b/packages/core/src/awsService/cloudformation/relatedResources/relatedResourcesProtocol.ts new file mode 100644 index 00000000000..d5ded1a5bff --- /dev/null +++ b/packages/core/src/awsService/cloudformation/relatedResources/relatedResourcesProtocol.ts @@ -0,0 +1,39 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RequestType, CodeAction, Position } from 'vscode-languageserver-protocol' + +export type TemplateUri = string + +export type GetRelatedResourceTypesParams = { + parentResourceType: string +} + +export type InsertRelatedResourcesParams = { + templateUri: string + relatedResourceTypes: string[] + parentResourceType: string +} + +export interface RelatedResourcesCodeAction extends CodeAction { + data?: { + scrollToPosition?: Position + firstLogicalId?: string + } +} + +export const GetAuthoredResourceTypesRequest = new RequestType( + 'aws/cfn/template/resources/authored' +) + +export const GetRelatedResourceTypesRequest = new RequestType( + 'aws/cfn/template/resources/related' +) + +export const InsertRelatedResourcesRequest = new RequestType< + InsertRelatedResourcesParams, + RelatedResourcesCodeAction, + void +>('aws/cfn/template/resources/insert') diff --git a/packages/core/src/awsService/cloudformation/resources/resourceRequestTypes.ts b/packages/core/src/awsService/cloudformation/resources/resourceRequestTypes.ts new file mode 100644 index 00000000000..9b64782526f --- /dev/null +++ b/packages/core/src/awsService/cloudformation/resources/resourceRequestTypes.ts @@ -0,0 +1,102 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RequestType, CompletionItem, TextDocumentIdentifier } from 'vscode-languageserver-protocol' + +export interface ResourceRequest { + resourceType: string + nextToken?: string +} + +export interface ListResourcesParams { + resources?: ResourceRequest[] +} + +export interface ResourceTypesParams {} + +export interface ResourceTypesResult { + resourceTypes: string[] +} + +export interface ResourceList { + typeName: string + resourceIdentifiers: string[] + nextToken?: string +} + +export interface ListResourcesResult { + resources: ResourceList[] +} + +export const ListResourcesRequest = new RequestType( + 'aws/cfn/resources/list' +) + +export const RefreshResourcesRequest = new RequestType( + 'aws/cfn/resources/refresh' +) + +export const ResourceTypesRequest = new RequestType( + 'aws/cfn/resources/types' +) + +export const RemoveResourceTypeRequest = new RequestType('aws/cfn/resources/list/remove') + +export type ResourceSelection = { + resourceType: string + resourceIdentifiers: string[] +} + +export enum ResourceStatePurpose { + Import = 'Import', + Clone = 'Clone', +} + +export interface ResourceStateParams { + textDocument: TextDocumentIdentifier + resourceSelections?: ResourceSelection[] + purpose: ResourceStatePurpose + parentResourceType?: string +} + +export type ResourceType = string +export type ResourceIdentifier = string + +export interface ResourceStateResult { + completionItem?: CompletionItem + successfulImports: Map + failedImports: Map + warning?: string +} + +export const ResourceStateRequest = new RequestType( + 'aws/cfn/resources/state' +) + +export type ResourceStackManagementResult = { + physicalResourceId: string + managedByStack: boolean | undefined + stackName?: string + stackId?: string + error?: string +} + +export const StackMgmtInfoRequest = new RequestType( + 'aws/cfn/resources/stackMgmtInfo' +) + +export type SearchResourceParams = { + resourceType: string + identifier: string +} + +export type SearchResourceResult = { + found: boolean + resource?: ResourceList +} + +export const SearchResourceRequest = new RequestType( + 'aws/cfn/resources/search' +) diff --git a/packages/core/src/awsService/cloudformation/resources/resourcesManager.ts b/packages/core/src/awsService/cloudformation/resources/resourcesManager.ts new file mode 100644 index 00000000000..bdba551c7cd --- /dev/null +++ b/packages/core/src/awsService/cloudformation/resources/resourcesManager.ts @@ -0,0 +1,484 @@ +/*! +import { getLogger } from '../../../shared/logger' + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ResourceSelectionResult, ResourceSelector } from '../ui/resourceSelector' +import { ResourceNode } from '../explorer/nodes/resourceNode' +import { LanguageClient } from 'vscode-languageclient/node' +import { + ListResourcesRequest, + RefreshResourcesRequest, + ResourceList, + ResourceSelection, + ResourceStackManagementResult, + ResourceStateParams, + ResourceStatePurpose, + ResourceStateRequest, + ResourceStateResult, + StackMgmtInfoRequest, + SearchResourceRequest, + SearchResourceResult, + RemoveResourceTypeRequest, +} from './resourceRequestTypes' + +import { handleLspError } from '../utils/onlineErrorHandler' +import { showErrorMessage } from '../ui/message' +import { ProgressLocation, SnippetString, window, env, Position, Range } from 'vscode' +import { getLogger } from '../../../shared/logger/logger' +import globals from '../../../shared/extensionGlobals' +import { setContext } from '../../../shared/vscode/setContext' + +type ResourcesChangeListener = (resources: ResourceList[]) => void + +export class ResourcesManager { + private resources: Map = new Map() + private readonly listeners: ResourcesChangeListener[] = [] + private static readonly resourceTypesKey = 'aws.cloudformation.selectedResourceTypes' + + private readonly CopyStackName = 'Copy Stack Name' + private readonly CopyStackArn = 'Copy Stack Arn' + + constructor( + private readonly client: LanguageClient, + private readonly resourceSelector: ResourceSelector + ) {} + + private get selectedResourceTypes(): string[] { + return globals.globalState.tryGet(ResourcesManager.resourceTypesKey, Object, []) + } + + private async setSelectedResourceTypes(types: string[]): Promise { + await globals.globalState.update(ResourcesManager.resourceTypesKey, types) + } + + getSelectedResourceTypes(): string[] { + return this.selectedResourceTypes + } + + async removeResourceType(typeToRemove: string): Promise { + await globals.globalState.update( + ResourcesManager.resourceTypesKey, + this.selectedResourceTypes.filter((type) => type !== typeToRemove) + ) + await this.client.sendRequest(RemoveResourceTypeRequest, typeToRemove) + this.notifyAllListeners() + } + + get(): ResourceList[] { + return Array.from(this.resources.values()) + } + + addListener(listener: ResourcesChangeListener) { + this.listeners.push(listener) + } + + async loadResources(): Promise { + try { + if (this.selectedResourceTypes.length === 0) { + this.resources.clear() + return + } + + this.resources.clear() + + const response = await this.client.sendRequest(ListResourcesRequest, { + resources: this.selectedResourceTypes.map((resourceType) => ({ resourceType })), + }) + + for (const resource of response.resources) { + this.resources.set(resource.typeName, resource) + } + } catch (error) { + await handleLspError(error, 'Error loading resources') + this.resources.clear() + } finally { + this.notifyAllListeners() + } + } + + async loadResourceType(resourceType: string): Promise { + const response = await this.client.sendRequest(ListResourcesRequest, { + resources: [{ resourceType }], + }) + + if (response.resources.length > 0) { + this.resources.set(resourceType, response.resources[0]) + this.notifyAllListeners() + } + } + + async loadMoreResources(resourceType: string, nextToken: string): Promise { + await setContext('aws.cloudformation.loadingResources', true) + try { + const response = await this.client.sendRequest(ListResourcesRequest, { + resources: [{ resourceType, nextToken }], + }) + + if (response.resources.length > 0) { + this.resources.set(resourceType, response.resources[0]) + } + + this.notifyAllListeners() + } catch (error) { + await handleLspError(error, 'Error loading more resources') + } finally { + await setContext('aws.cloudformation.loadingResources', false) + } + } + + async refreshAllResources(): Promise { + await window.withProgress( + { + location: ProgressLocation.Notification, + title: 'Refreshing All Resources List', + }, + async () => { + await setContext('aws.cloudformation.refreshingAllResources', true) + try { + if (this.selectedResourceTypes.length === 0) { + return + } + + const response = await this.client.sendRequest(RefreshResourcesRequest, { + resources: this.selectedResourceTypes.map((resourceType) => ({ resourceType })), + }) + this.resources.clear() + for (const resource of response.resources) { + this.resources.set(resource.typeName, resource) + } + } finally { + await setContext('aws.cloudformation.refreshingAllResources', false) + this.notifyAllListeners() + } + } + ) + } + + async refreshResourceList(resourceType: string): Promise { + await window.withProgress( + { + location: ProgressLocation.Notification, + title: `Refreshing ${resourceType} Resources List`, + }, + async () => { + await setContext('aws.cloudformation.refreshingResourceList', true) + try { + const response = await this.client.sendRequest(RefreshResourcesRequest, { + resources: [{ resourceType }], + }) + + const updatedResource = response.resources.find( + (r: { typeName: string }) => r.typeName === resourceType + ) + if (updatedResource) { + this.resources.set(resourceType, updatedResource) + } + } finally { + await setContext('aws.cloudformation.refreshingResourceList', false) + this.notifyAllListeners() + } + } + ) + } + + async searchResource(resourceType: string, identifier: string): Promise { + try { + const response = await this.client.sendRequest(SearchResourceRequest, { + resourceType, + identifier, + }) + + if (response.found && response.resource) { + this.resources.set(resourceType, response.resource) + this.notifyAllListeners() + } + + return response + } catch (error) { + getLogger().error(`Failed to search resource: ${error}`) + return { found: false } + } + } + + async selectResourceTypes(): Promise { + const selectedTypes = await this.resourceSelector.selectResourceTypes(this.selectedResourceTypes) + if (selectedTypes !== undefined) { + await this.setSelectedResourceTypes(selectedTypes) + + // Remove resources that are no longer selected + const selectedSet = new Set(selectedTypes) + for (const typeName of this.resources.keys()) { + if (!selectedSet.has(typeName)) { + this.resources.delete(typeName) + } + } + + this.notifyAllListeners() + } + } + + private async executeResourceStateOperation( + resourceNodes: ResourceNode[] | undefined, + purpose: ResourceStatePurpose, + parentResourceType?: string + ): Promise { + const editor = window.activeTextEditor + if (!editor) { + showErrorMessage('Open a CloudFormation template to author resource state') + return + } + + const contextKey = + purpose === ResourceStatePurpose.Import + ? 'aws.cloudformation.importingResource' + : 'aws.cloudformation.cloningResource' + await setContext(contextKey, true) + + try { + const resourceSelectionsArray = await this.getResourceSelectionArray(resourceNodes) + if (resourceSelectionsArray.length === 0) { + return + } + + const params: ResourceStateParams = { + textDocument: { uri: editor.document.uri.toString() }, + resourceSelections: resourceSelectionsArray, + purpose, + parentResourceType, + } + + const title = + purpose === ResourceStatePurpose.Import ? 'Importing Resource State' : 'Cloning Resource State' + await window.withProgress( + { + location: ProgressLocation.Notification, + title, + cancellable: false, + }, + async () => { + const result = (await this.client.sendRequest( + ResourceStateRequest.method, + params + )) as ResourceStateResult + if (result.warning) { + void window.showWarningMessage(result.warning) + } + await this.applyCompletionSnippet(result) + const [successCount, failureCount] = this.getSuccessAndFailureCount(result) + this.renderResultMessage(successCount, failureCount, purpose) + } + ) + } catch (error) { + const action = purpose === ResourceStatePurpose.Import ? 'importing' : 'cloning' + showErrorMessage( + `Error ${action} resource state: ${error instanceof Error ? error.message : String(error)}` + ) + } finally { + await setContext(contextKey, false) + } + } + + async importResourceStates(resourceNodes?: ResourceNode[], parentResourceType?: string): Promise { + await this.executeResourceStateOperation(resourceNodes, ResourceStatePurpose.Import, parentResourceType) + } + + private getResourcesToImportInput(selections: ResourceSelectionResult[]): ResourceSelection[] { + // Group selections by resource type + const resourceSelections = new Map() + for (const selection of selections) { + const identifiers = resourceSelections.get(selection.resourceType) ?? [] + identifiers.push(selection.resourceIdentifier) + resourceSelections.set(selection.resourceType, identifiers) + } + + // Convert to ResourceSelection[] format expected by server + return Array.from(resourceSelections.entries()).map(([resourceType, resourceIdentifiers]) => ({ + resourceType, + resourceIdentifiers, + })) + } + + private async applyCompletionSnippet(result: ResourceStateResult): Promise { + const { completionItem } = result + + if (!completionItem?.textEdit) { + getLogger().warn('No completionItem or textEdit in result') + return + } + + const editor = window.activeTextEditor + if (!editor) { + getLogger().warn('No active editor for snippet insertion') + return + } + + try { + const textEdit = completionItem.textEdit + if (!textEdit || !('range' in textEdit)) { + getLogger().warn('No valid textEdit range found') + return + } + + const targetLine = textEdit.range.start.line + await this.ensureLineExists(editor, targetLine) + + const range = new Range( + new Position(textEdit.range.start.line, textEdit.range.start.character), + new Position(textEdit.range.end.line, textEdit.range.end.character) + ) + + getLogger().info( + `Inserting snippet at server-provided position: line ${range.start.line}, char ${range.start.character}` + ) + await editor.insertSnippet(new SnippetString(textEdit.newText), range) + getLogger().info('Snippet insertion successful') + } catch (error) { + getLogger().error(`Failed to insert snippet: ${error instanceof Error ? error.message : String(error)}`) + showErrorMessage(`Failed to insert resource: ${error instanceof Error ? error.message : String(error)}`) + } + } + + private async ensureLineExists(editor: any, targetLine: number): Promise { + const document = editor.document + if (targetLine >= document.lineCount) { + const linesToAdd = targetLine - document.lineCount + 1 + const lastLine = document.lineAt(document.lineCount - 1) + const endPosition = lastLine.range.end + + await editor.edit((editBuilder: any) => { + editBuilder.insert(endPosition, '\n'.repeat(linesToAdd)) + }) + } + } + + private getSuccessAndFailureCount(result: ResourceStateResult): [number, number] { + const successCount = Object.values(result.successfulImports ?? {}).reduce( + (sum: number, ids: string[]) => sum + ids.length, + 0 + ) as number + const failureCount = Object.values(result.failedImports ?? {}).reduce( + (sum: number, ids: string[]) => sum + ids.length, + 0 + ) as number + return [successCount, failureCount] + } + + async cloneResourceStates(resourceNodes?: ResourceNode[]): Promise { + await this.executeResourceStateOperation(resourceNodes, ResourceStatePurpose.Clone) + } + + private async getResourceSelectionArray(resourceNodes?: ResourceNode[]): Promise { + let selections: ResourceSelectionResult[] + + if (resourceNodes?.length) { + selections = resourceNodes.map((node) => ({ + resourceType: node.resourceType, + resourceIdentifier: node.resourceIdentifier, + })) + } else { + selections = await this.resourceSelector.selectResources(true, undefined, { + getCached: (type) => this.resources.get(type), + loadMore: (type, token) => this.loadMoreResources(type, token), + search: (type, id) => this.searchResource(type, id), + }) + } + + if (selections.length === 0) { + return [] + } + + return this.getResourcesToImportInput(selections) + } + + private renderResultMessage(successCount: number, failureCount: number, purpose: ResourceStatePurpose) { + const action = purpose === ResourceStatePurpose.Import ? 'imported' : 'cloned' + + if (successCount > 0 && failureCount === 0) { + void window.showInformationMessage(`Successfully ${action} ${successCount} resource(s)`) + } else if (successCount > 0 && failureCount > 0) { + void window.showWarningMessage( + `${action.charAt(0).toUpperCase() + action.slice(1)} ${successCount} resource(s), ${failureCount} failed` + ) + } else if (failureCount > 0) { + showErrorMessage(`Failed to ${action.replace('ed', '')} ${failureCount} resource(s)`) + } else { + void window.showInformationMessage(`No resources were ${action}`) + } + } + + private getResourcesArray(): ResourceList[] { + return Array.from(this.resources.values()) + } + + private notifyAllListeners(): void { + for (const listener of this.listeners) { + listener(this.getResourcesArray()) + } + } + + reload() { + this.resources.clear() + this.notifyAllListeners() + } + + async getStackManagementInfo(resourceNode?: ResourceNode): Promise { + let resourceIdentifier: string | undefined + + if (resourceNode?.resourceIdentifier) { + resourceIdentifier = resourceNode.resourceIdentifier + } else { + const selection = await this.resourceSelector.selectSingleResource({ + getCached: (type) => this.resources.get(type), + loadMore: (type, token) => this.loadMoreResources(type, token), + search: (type, id) => this.searchResource(type, id), + }) + if (!selection) { + return + } + resourceIdentifier = selection.resourceIdentifier + } + + await setContext('aws.cloudformation.gettingStackMgmtInfo', true) + try { + const result = (await window.withProgress( + { + location: ProgressLocation.SourceControl, + title: 'Getting Stack Management Info', + cancellable: false, + }, + async () => { + return await this.client.sendRequest(StackMgmtInfoRequest.method, resourceIdentifier) + } + )) as ResourceStackManagementResult + + await setContext('aws.cloudformation.gettingStackMgmtInfo', false) + + if (result.managedByStack === true && result.stackName && result.stackId) { + const action = await window.showInformationMessage( + `${result.physicalResourceId} is managed by stack: ${result.stackName}`, + this.CopyStackName, + this.CopyStackArn + ) + + if (action === this.CopyStackName) { + await env.clipboard.writeText(result.stackName) + window.setStatusBarMessage('Stack name copied to clipboard', 3000) + } else if (action === this.CopyStackArn) { + await env.clipboard.writeText(result.stackId) + window.setStatusBarMessage('Stack ARN copied to clipboard', 3000) + } + } else if (result.managedByStack === false) { + void window.showInformationMessage(`${result.physicalResourceId} is not managed by any stack`) + } else { + showErrorMessage(`Failed to determine stack management status: ${result.error ?? 'Unknown error'}`) + } + } catch (error) { + showErrorMessage( + `Error getting stack management info: ${error instanceof Error ? error.message : String(error)}` + ) + await setContext('aws.cloudformation.gettingStackMgmtInfo', false) + } + } +} diff --git a/packages/core/src/awsService/cloudformation/stacks/actions/changeSetDeletionWorkflow.ts b/packages/core/src/awsService/cloudformation/stacks/actions/changeSetDeletionWorkflow.ts new file mode 100644 index 00000000000..a1fa6bdd65c --- /dev/null +++ b/packages/core/src/awsService/cloudformation/stacks/actions/changeSetDeletionWorkflow.ts @@ -0,0 +1,95 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { v4 as uuidv4 } from 'uuid' +import { StackActionPhase, StackActionState } from './stackActionRequestType' +import { LanguageClient } from 'vscode-languageclient/node' +import { + showErrorMessage, + showChangeSetDeletionStarted, + showChangeSetDeletionSuccess, + showChangeSetDeletionFailure, +} from '../../ui/message' +import { deleteChangeSet, describeChangeSetDeletionStatus, getChangeSetDeletionStatus } from './stackActionApi' +import { createChangeSetDeletionParams } from './stackActionUtil' +import { getLogger } from '../../../../shared/logger/logger' +import { commandKey, extractErrorMessage } from '../../utils' +import { commands } from 'vscode' +import globals from '../../../../shared/extensionGlobals' + +export class ChangeSetDeletion { + private readonly id: string + private status: StackActionPhase | undefined + + constructor( + private readonly stackName: string, + private readonly changeSetName: string, + private readonly client: LanguageClient + ) { + this.id = uuidv4() + } + + async delete() { + await deleteChangeSet(this.client, createChangeSetDeletionParams(this.id, this.stackName, this.changeSetName)) + showChangeSetDeletionStarted(this.changeSetName, this.stackName) + this.pollForProgress() + } + + private pollForProgress() { + const interval = globals.clock.setInterval(() => { + getChangeSetDeletionStatus(this.client, { id: this.id }) + .then(async (deletionResult) => { + if (deletionResult.phase === this.status) { + return + } + + this.status = deletionResult.phase + + switch (deletionResult.phase) { + case StackActionPhase.DELETION_IN_PROGRESS: + break + case StackActionPhase.DELETION_COMPLETE: + if (deletionResult.state === StackActionState.SUCCESSFUL) { + showChangeSetDeletionSuccess(this.changeSetName, this.stackName) + } else { + const describeDeplomentStatusResult = await describeChangeSetDeletionStatus( + this.client, + { + id: this.id, + } + ) + showChangeSetDeletionFailure( + this.changeSetName, + this.stackName, + describeDeplomentStatusResult.FailureReason ?? 'No failure reason provided' + ) + } + void commands.executeCommand(commandKey('stacks.refresh')) + globals.clock.clearInterval(interval) + break + case StackActionPhase.DELETION_FAILED: { + const describeDeplomentStatusResult = await describeChangeSetDeletionStatus(this.client, { + id: this.id, + }) + showChangeSetDeletionFailure( + this.changeSetName, + this.stackName, + describeDeplomentStatusResult.FailureReason ?? 'No failure reason provided' + ) + void commands.executeCommand(commandKey('stacks.refresh')) + globals.clock.clearInterval(interval) + break + } + } + }) + .catch(async (error) => { + getLogger().error(`Error polling for deletion status: ${error}`) + showErrorMessage(`Error polling for deletion status: ${extractErrorMessage(error)}`) + void commands.executeCommand(commandKey('stacks.refresh')) + globals.clock.clearInterval(interval) + }) + }, 1000) + } +} diff --git a/packages/core/src/awsService/cloudformation/stacks/actions/deploymentWorkflow.ts b/packages/core/src/awsService/cloudformation/stacks/actions/deploymentWorkflow.ts new file mode 100644 index 00000000000..e4e790a56dd --- /dev/null +++ b/packages/core/src/awsService/cloudformation/stacks/actions/deploymentWorkflow.ts @@ -0,0 +1,100 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { v4 as uuidv4 } from 'uuid' +import { StackActionPhase, StackActionState } from './stackActionRequestType' +import { LanguageClient } from 'vscode-languageclient/node' +import { showDeploymentStarted, showDeploymentSuccess, showDeploymentFailure, showErrorMessage } from '../../ui/message' +import { createDeploymentStatusBar, updateWorkflowStatus } from '../../ui/statusBar' +import { commands } from 'vscode' +import { deploy, describeDeploymentStatus, getDeploymentStatus } from './stackActionApi' +import { createDeploymentParams } from './stackActionUtil' +import { getLogger } from '../../../../shared/logger/logger' +import { extractErrorMessage, commandKey } from '../../utils' +import { StackViewCoordinator } from '../../ui/stackViewCoordinator' + +export class Deployment { + private readonly id: string + private status: StackActionPhase | undefined + private statusBarHandle?: { update(phase: StackActionPhase): void; release(): void } + + constructor( + private readonly stackName: string, + private readonly changeSetName: string, + private readonly client: LanguageClient, + private readonly coordinator: StackViewCoordinator + ) { + this.id = uuidv4() + } + + async deploy() { + await deploy(this.client, createDeploymentParams(this.id, this.stackName, this.changeSetName)) + showDeploymentStarted(this.stackName) + + await this.coordinator.setStack(this.stackName) + await commands.executeCommand(commandKey('stack.events.focus')) + + this.statusBarHandle = createDeploymentStatusBar(this.stackName, 'Deployment', this.changeSetName) + this.pollForProgress() + } + + private pollForProgress() { + const interval = setInterval(() => { + getDeploymentStatus(this.client, { id: this.id }) + .then(async (deploymentResult) => { + if (deploymentResult.phase === this.status) { + return + } + + this.status = deploymentResult.phase + if (this.statusBarHandle) { + updateWorkflowStatus(this.statusBarHandle, deploymentResult.phase) + } + + switch (deploymentResult.phase) { + case StackActionPhase.DEPLOYMENT_IN_PROGRESS: + break + case StackActionPhase.DEPLOYMENT_COMPLETE: + if (deploymentResult.state === StackActionState.SUCCESSFUL) { + showDeploymentSuccess(this.stackName) + } else { + const describeDeploymentStatusResult = await describeDeploymentStatus(this.client, { + id: this.id, + }) + showDeploymentFailure( + this.stackName, + describeDeploymentStatusResult.FailureReason ?? 'UNKNOWN' + ) + } + void commands.executeCommand(commandKey('stacks.refresh')) + this.statusBarHandle?.release() + clearInterval(interval) + break + case StackActionPhase.DEPLOYMENT_FAILED: + case StackActionPhase.VALIDATION_FAILED: { + const describeDeplomentStatusResult = await describeDeploymentStatus(this.client, { + id: this.id, + }) + showDeploymentFailure( + this.stackName, + describeDeplomentStatusResult.FailureReason ?? 'UNKNOWN' + ) + void commands.executeCommand(commandKey('stacks.refresh')) + this.statusBarHandle?.release() + clearInterval(interval) + break + } + } + }) + .catch(async (error) => { + getLogger().error(`Error polling for deployment status: ${error}`) + showErrorMessage(`Error polling for deployment status: ${extractErrorMessage(error)}`) + void commands.executeCommand(commandKey('stacks.refresh')) + this.statusBarHandle?.release() + clearInterval(interval) + }) + }, 1000) + } +} diff --git a/packages/core/src/awsService/cloudformation/stacks/actions/stackActionApi.ts b/packages/core/src/awsService/cloudformation/stacks/actions/stackActionApi.ts new file mode 100644 index 00000000000..0ca569de95a --- /dev/null +++ b/packages/core/src/awsService/cloudformation/stacks/actions/stackActionApi.ts @@ -0,0 +1,127 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LanguageClient } from 'vscode-languageclient/node' +import { + TemplateUri, + GetParametersResult, + GetCapabilitiesResult, + CreateStackActionResult, + GetStackActionStatusResult, + TemplateResource, + CreateValidationParams, + CreateDeploymentParams, + DescribeValidationStatusResult, + DescribeDeploymentStatusResult, + DeleteChangeSetParams, + DescribeDeletionStatusResult, + DescribeChangeSetParams, + DescribeChangeSetResult, + GetTemplateArtifactsResult, +} from './stackActionRequestType' +import { + GetParametersRequest, + GetCapabilitiesRequest, + CreateValidationRequest, + CreateDeploymentRequest, + GetValidationStatusRequest, + GetDeploymentStatusRequest, + GetTemplateResourcesRequest, + GetTemplateArtifactsRequest, + DescribeValidationStatusRequest, + DescribeDeploymentStatusRequest, + DeleteChangeSetRequest, + GetChangeSetDeletionStatusRequest, + DescribeChangeSetDeletionStatusRequest, + DescribeChangeSetRequest, +} from './stackActionProtocol' +import { Identifiable } from '../../lspTypes' + +export async function validate( + client: LanguageClient, + params: CreateValidationParams +): Promise { + return await client.sendRequest(CreateValidationRequest, params) +} + +export async function deploy(client: LanguageClient, params: CreateDeploymentParams): Promise { + return await client.sendRequest(CreateDeploymentRequest, params) +} + +export async function deleteChangeSet( + client: LanguageClient, + params: DeleteChangeSetParams +): Promise { + return await client.sendRequest(DeleteChangeSetRequest, params) +} + +export async function getValidationStatus( + client: LanguageClient, + params: Identifiable +): Promise { + return await client.sendRequest(GetValidationStatusRequest, params) +} + +export async function getDeploymentStatus( + client: LanguageClient, + params: Identifiable +): Promise { + return await client.sendRequest(GetDeploymentStatusRequest, params) +} + +export async function describeValidationStatus( + client: LanguageClient, + params: Identifiable +): Promise { + return await client.sendRequest(DescribeValidationStatusRequest, params) +} + +export async function describeDeploymentStatus( + client: LanguageClient, + params: Identifiable +): Promise { + return await client.sendRequest(DescribeDeploymentStatusRequest, params) +} + +export async function getChangeSetDeletionStatus( + client: LanguageClient, + params: Identifiable +): Promise { + return await client.sendRequest(GetChangeSetDeletionStatusRequest, params) +} + +export async function describeChangeSetDeletionStatus( + client: LanguageClient, + params: Identifiable +): Promise { + return await client.sendRequest(DescribeChangeSetDeletionStatusRequest, params) +} + +export async function getParameters(client: LanguageClient, params: TemplateUri): Promise { + return await client.sendRequest(GetParametersRequest, params) +} + +export async function getCapabilities(client: LanguageClient, params: TemplateUri): Promise { + return await client.sendRequest(GetCapabilitiesRequest, params) +} + +export async function getTemplateResources(client: LanguageClient, params: TemplateUri): Promise { + const result = await client.sendRequest(GetTemplateResourcesRequest, params) + return result.resources +} + +export async function getTemplateArtifacts( + client: LanguageClient, + params: TemplateUri +): Promise { + return await client.sendRequest(GetTemplateArtifactsRequest, params) +} + +export async function describeChangeSet( + client: LanguageClient, + params: DescribeChangeSetParams +): Promise { + return await client.sendRequest(DescribeChangeSetRequest, params) +} diff --git a/packages/core/src/awsService/cloudformation/stacks/actions/stackActionInputValidation.ts b/packages/core/src/awsService/cloudformation/stacks/actions/stackActionInputValidation.ts new file mode 100644 index 00000000000..a03e2eaa12e --- /dev/null +++ b/packages/core/src/awsService/cloudformation/stacks/actions/stackActionInputValidation.ts @@ -0,0 +1,116 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fs } from '../../../../shared/fs/fs' +import { TemplateParameter } from './stackActionRequestType' + +export function validateTemplatePath(value: string): string | undefined { + if (!value) { + return 'Template path is required' + } + + const filePath = value.startsWith('file://') ? value.slice(7) : value + if (!fs.exists(filePath)) { + return 'Template file does not exist' + } + + const validExtensions = ['.yaml', '.json', '.yml', '.txt', '.cfn', '.template'] + if (!validExtensions.some((ext) => filePath.endsWith(ext))) { + return 'Invalid template file extension' + } + + return undefined +} + +export function validateStackName(value: string): string | undefined { + if (!value) { + return 'Stack name is required' + } + + if (value.length > 128) { + return 'Stack name must be 128 characters or less' + } + + if (!/^[a-zA-Z][-a-zA-Z0-9]*$/.test(value)) { + return 'Stack name must start with a letter and contain only alphanumeric characters and hyphens' + } + + return undefined +} + +export function validateChangeSetName(value: string): string | undefined { + if (!value) { + return 'Change Set name is required' + } + + if (value.length > 128) { + return 'Change Set name must be 128 characters or less' + } + + if (!/^[a-zA-Z][-a-zA-Z0-9]*$/.test(value)) { + return 'Change Set name must start with a letter and contain only alphanumeric characters and hyphens' + } + + return undefined +} + +export function validateParameterValue(input: string, param: TemplateParameter): string | undefined { + const actualValue = input ?? param.Default?.toString() ?? '' + + // Handle CommaDelimitedList validation + if (param.Type === 'CommaDelimitedList') { + const items = actualValue.split(',').map((s) => s.trim()) + + if (param.AllowedValues) { + const allowedStrings = param.AllowedValues.map(String) + const invalidItems = items.filter((item) => !allowedStrings.includes(item)) + if (invalidItems.length > 0) { + return `Invalid values: ${invalidItems.join(', ')}. Must be one of: ${param.AllowedValues.join(', ')}` + } + } + + if (param.AllowedPattern) { + const pattern = new RegExp(param.AllowedPattern) + const invalidItems = items.filter((item) => !pattern.test(item)) + if (invalidItems.length > 0) { + return `Values must match pattern: ${param.AllowedPattern}` + } + } + + return undefined + } + + // Handle other types + if (param.AllowedValues && !param.AllowedValues.map(String).includes(actualValue)) { + return `Value must be one of: ${param.AllowedValues.join(', ')}` + } + + if (param.AllowedPattern && !new RegExp(param.AllowedPattern).test(actualValue)) { + return `Value must match pattern: ${param.AllowedPattern}` + } + + if (param.MinLength && actualValue.length < param.MinLength) { + return `Value must be at least ${param.MinLength} characters` + } + + if (param.MaxLength && actualValue.length > param.MaxLength) { + return `Value must be at most ${param.MaxLength} characters` + } + + if (param.Type === 'Number') { + const numValue = Number(actualValue) + if (isNaN(numValue)) { + return 'Value must be a number' + } + if (param.MinValue && numValue < param.MinValue) { + return `Value must be at least ${param.MinValue}` + } + if (param.MaxValue && numValue > param.MaxValue) { + return `Value must be at most ${param.MaxValue}` + } + } + + return undefined +} diff --git a/packages/core/src/awsService/cloudformation/stacks/actions/stackActionProtocol.ts b/packages/core/src/awsService/cloudformation/stacks/actions/stackActionProtocol.ts new file mode 100644 index 00000000000..e01a0423f89 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/stacks/actions/stackActionProtocol.ts @@ -0,0 +1,105 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RequestType } from 'vscode-languageserver-protocol' +import { Identifiable } from '../../lspTypes' +import { + TemplateUri, + GetParametersResult, + CreateStackActionResult, + GetStackActionStatusResult, + GetCapabilitiesResult, + GetTemplateResourcesResult, + GetTemplateArtifactsResult, + ListChangeSetsParams, + ListChangeSetsResult, + CreateValidationParams, + CreateDeploymentParams, + DescribeValidationStatusResult, + DescribeDeploymentStatusResult, + DeleteChangeSetParams, + DescribeDeletionStatusResult, + GetStackEventsParams, + GetStackEventsResult, + ClearStackEventsParams, + DescribeChangeSetParams, + DescribeChangeSetResult, + GetStackResourcesParams, + ListStackResourcesResult, + DescribeStackParams, + DescribeStackResult, +} from './stackActionRequestType' + +export const CreateValidationRequest = new RequestType( + 'aws/cfn/stack/validation/create' +) + +export const CreateDeploymentRequest = new RequestType( + 'aws/cfn/stack/deployment/create' +) + +export const GetValidationStatusRequest = new RequestType( + 'aws/cfn/stack/validation/status' +) + +export const GetDeploymentStatusRequest = new RequestType( + 'aws/cfn/stack/deployment/status' +) + +export const DescribeValidationStatusRequest = new RequestType( + 'aws/cfn/stack/validation/status/describe' +) + +export const DescribeDeploymentStatusRequest = new RequestType( + 'aws/cfn/stack/deployment/status/describe' +) + +export const DeleteChangeSetRequest = new RequestType( + 'aws/cfn/stack/changeSet/delete' +) + +export const GetChangeSetDeletionStatusRequest = new RequestType( + 'aws/cfn/stack/changeSet/deletion/status' +) + +export const DescribeChangeSetDeletionStatusRequest = new RequestType( + 'aws/cfn/stack/changeSet/deletion/status/describe' +) + +export const GetParametersRequest = new RequestType('aws/cfn/stack/parameters') + +export const GetCapabilitiesRequest = new RequestType( + 'aws/cfn/stack/capabilities' +) + +export const GetTemplateResourcesRequest = new RequestType( + 'aws/cfn/stack/import/resources' +) + +export const GetTemplateArtifactsRequest = new RequestType( + 'aws/cfn/stack/template/artifacts' +) + +export const ListChangeSetsRequest = new RequestType( + 'aws/cfn/stack/changeSet/list' +) + +export const GetStackEventsRequest = new RequestType( + 'aws/cfn/stack/events' +) + +export const ClearStackEventsRequest = new RequestType('aws/cfn/stack/events/clear') + +export const DescribeStackRequest = new RequestType( + 'aws/cfn/stack/describe' +) + +export const DescribeChangeSetRequest = new RequestType( + 'aws/cfn/stack/changeSet/describe' +) + +export const GetStackResourcesRequest = new RequestType( + 'aws/cfn/stack/resources' +) diff --git a/packages/core/src/awsService/cloudformation/stacks/actions/stackActionRequestType.ts b/packages/core/src/awsService/cloudformation/stacks/actions/stackActionRequestType.ts new file mode 100644 index 00000000000..4fb6d5e2c48 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/stacks/actions/stackActionRequestType.ts @@ -0,0 +1,297 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + Parameter, + Capability, + ResourceChangeDetail, + ResourceStatus, + DetailedStatus, + ResourceTargetDefinition, + StackEvent, + OnStackFailure, + Tag, + Stack, +} from '@aws-sdk/client-cloudformation' +import { Identifiable } from '../../lspTypes' + +export type ResourceToImport = { + ResourceType: string + LogicalResourceId: string + ResourceIdentifier: Record +} + +export enum DeploymentMode { + REVERT_DRIFT = 'REVERT_DRIFT', +} + +export type ChangeSetOptionalFlags = { + onStackFailure?: OnStackFailure + includeNestedStacks?: boolean + tags?: Tag[] + importExistingResources?: boolean + deploymentMode?: DeploymentMode +} + +export type CreateValidationParams = Identifiable & { + uri: string + stackName: string + parameters?: Parameter[] + capabilities?: Capability[] + resourcesToImport?: ResourceToImport[] + keepChangeSet?: boolean + onStackFailure?: OnStackFailure + includeNestedStacks?: boolean + tags?: Tag[] + importExistingResources?: boolean + deploymentMode?: DeploymentMode + s3Bucket?: string + s3Key?: string +} + +export type ChangeSetReference = { + changeSetName: string + stackName: string +} + +export type CreateDeploymentParams = Identifiable & ChangeSetReference + +export type DeleteChangeSetParams = Identifiable & ChangeSetReference + +export type CreateStackActionResult = Identifiable & ChangeSetReference + +export type ValidationResult = { + level: 'FAIL' | 'WARN' | 'INFO' + type: string + validationName: string + status: 'COMPLETE' | 'FAILED' | 'SKIPPED' + details: string + propertyPath?: string + remediationAction?: string + detailedStatus?: string +} + +export type StackChange = { + type?: string + resourceChange?: { + action?: string + logicalResourceId?: string + physicalResourceId?: string + resourceType?: string + replacement?: string + scope?: string[] + beforeContext?: string + afterContext?: string + resourceDriftStatus?: string + details?: ResourceChangeDetailV2[] + } + validationResults?: ValidationResult[] +} + +export type ResourceTargetDefinitionV2 = ResourceTargetDefinition & { + Drift?: { + PreviousValue: string + ActualValue?: string + } + LiveResourceDrift?: { + PreviousValue: string + ActualValue?: string + } +} + +export type ResourceChangeDetailV2 = Omit & { + Target?: ResourceTargetDefinitionV2 +} + +export enum StackActionPhase { + VALIDATION_STARTED = 'VALIDATION_STARTED', + VALIDATION_IN_PROGRESS = 'VALIDATION_IN_PROGRESS', + VALIDATION_COMPLETE = 'VALIDATION_COMPLETE', + VALIDATION_FAILED = 'VALIDATION_FAILED', + DEPLOYMENT_STARTED = 'DEPLOYMENT_STARTED', + DEPLOYMENT_IN_PROGRESS = 'DEPLOYMENT_IN_PROGRESS', + DEPLOYMENT_COMPLETE = 'DEPLOYMENT_COMPLETE', + DEPLOYMENT_FAILED = 'DEPLOYMENT_FAILED', + DELETION_STARTED = 'DELETION_STARTED', + DELETION_IN_PROGRESS = 'DELETION_IN_PROGRESS', + DELETION_COMPLETE = 'DELETION_COMPLETE', + DELETION_FAILED = 'DELETION_FAILED', +} + +export enum StackActionState { + IN_PROGRESS = 'IN_PROGRESS', + SUCCESSFUL = 'SUCCESSFUL', + FAILED = 'FAILED', +} + +export type GetStackActionStatusResult = Identifiable & { + phase: StackActionPhase + state: StackActionState + changes?: StackChange[] +} + +export type ValidationDetail = { + ValidationName: string + LogicalId?: string + ResourcePropertyPath?: string + Severity: 'INFO' | 'ERROR' + Message: string +} + +export type DeploymentEvent = { + LogicalResourceId?: string + ResourceType?: string + ResourceStatus?: ResourceStatus + ResourceStatusReason?: string + DetailedStatus?: DetailedStatus +} + +export type Failable = { + FailureReason?: string +} + +export type DescribeValidationStatusResult = GetStackActionStatusResult & + Failable & { + ValidationDetails?: ValidationDetail[] + deploymentMode?: DeploymentMode + } + +export type DescribeDeploymentStatusResult = GetStackActionStatusResult & + Failable & { + DeploymentEvents?: DeploymentEvent[] + } + +export type DescribeDeletionStatusResult = GetStackActionStatusResult & Failable + +export type GetParametersResult = { + parameters: TemplateParameter[] +} + +export type GetCapabilitiesResult = { + capabilities: Capability[] +} + +export type TemplateResource = { + logicalId: string + type: string + primaryIdentifierKeys?: string[] + primaryIdentifier?: Record +} + +export type GetTemplateResourcesResult = { + resources: TemplateResource[] +} + +export type Artifact = { + resourceType: string + filePath: string +} + +export type GetTemplateArtifactsResult = { + artifacts: Artifact[] +} + +export enum OptionalFlagMode { + Skip = 'Skip for now', + Input = 'Input optional flags', + DevFriendly = 'Use default developer friendly flags', +} + +export type TemplateParameter = { + name: string + Type?: string + Default?: string | number | boolean + Description?: string + AllowedValues?: (string | number | boolean)[] + AllowedPattern?: string + MinLength?: number + MaxLength?: number + MinValue?: number + MaxValue?: number +} + +export type TemplateUri = string + +export type ChangeSetInfo = { + changeSetName: string + status: string + creationTime?: string + description?: string +} + +export type ListChangeSetsParams = { + stackName: string + nextToken?: string +} + +export type ListChangeSetsResult = { + changeSets: ChangeSetInfo[] + nextToken?: string +} + +export type DescribeChangeSetParams = ChangeSetReference + +export type DescribeChangeSetResult = ChangeSetInfo & { + stackName: string + changes?: StackChange[] + deploymentMode?: DeploymentMode +} + +export type StackInfo = { + StackName: string + StackId?: string + StackStatus?: string + StackStatusReason?: string + TemplateDescription?: string + CreationTime?: string + LastUpdatedTime?: string + RootId?: string + ParentId?: string + DisableRollback?: boolean + EnableTerminationProtection?: boolean + TimeoutInMinutes?: number +} + +export type GetStackEventsParams = { + stackName: string + nextToken?: string + refresh?: boolean +} + +export type GetStackEventsResult = { + events: StackEvent[] + nextToken?: string + gapDetected?: boolean +} + +export type ClearStackEventsParams = { + stackName: string +} + +export type DescribeStackParams = { + stackName: string +} + +export type DescribeStackResult = { + stack?: Stack +} + +export interface StackResourceSummary { + LogicalResourceId: string + PhysicalResourceId?: string + ResourceType: string + ResourceStatus: string + Timestamp?: string +} + +export type ListStackResourcesResult = { + resources: StackResourceSummary[] + nextToken?: string +} + +export interface GetStackResourcesParams { + stackName: string + nextToken?: string +} diff --git a/packages/core/src/awsService/cloudformation/stacks/actions/stackActionUtil.ts b/packages/core/src/awsService/cloudformation/stacks/actions/stackActionUtil.ts new file mode 100644 index 00000000000..9a88bb71aa7 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/stacks/actions/stackActionUtil.ts @@ -0,0 +1,55 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeSetOptionalFlags, + CreateDeploymentParams, + CreateValidationParams, + DeleteChangeSetParams, + ResourceToImport, +} from './stackActionRequestType' +import { Capability, Parameter } from '@aws-sdk/client-cloudformation' + +export function createValidationParams( + id: string, + uri: string, + stackName: string, + parameters?: Parameter[], + capabilities?: Capability[], + resourcesToImport?: ResourceToImport[], + keepChangeSet?: boolean, + optionalFlags?: ChangeSetOptionalFlags, + s3Bucket?: string, + s3Key?: string +): CreateValidationParams { + return { + id, + uri, + stackName, + parameters, + capabilities, + resourcesToImport, + keepChangeSet, + onStackFailure: optionalFlags?.onStackFailure, + includeNestedStacks: optionalFlags?.includeNestedStacks, + tags: optionalFlags?.tags, + importExistingResources: optionalFlags?.importExistingResources, + deploymentMode: optionalFlags?.deploymentMode, + s3Bucket, + s3Key, + } +} + +export function createDeploymentParams(id: string, stackName: string, changeSetName: string): CreateDeploymentParams { + return { id, stackName, changeSetName } +} + +export function createChangeSetDeletionParams( + id: string, + stackName: string, + changeSetName: string +): DeleteChangeSetParams { + return { id, stackName, changeSetName } +} diff --git a/packages/core/src/awsService/cloudformation/stacks/actions/validationWorkflow.ts b/packages/core/src/awsService/cloudformation/stacks/actions/validationWorkflow.ts new file mode 100644 index 00000000000..70f0565678a --- /dev/null +++ b/packages/core/src/awsService/cloudformation/stacks/actions/validationWorkflow.ts @@ -0,0 +1,171 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { v4 as uuidv4 } from 'uuid' +import { Parameter, Capability, ChangeSetStatus } from '@aws-sdk/client-cloudformation' +import { + StackActionPhase, + StackChange, + StackActionState, + ResourceToImport, + ChangeSetOptionalFlags, + ValidationDetail, + DeploymentMode, +} from './stackActionRequestType' +import { LanguageClient } from 'vscode-languageclient/node' +import { showErrorMessage, showValidationStarted, showValidationSuccess, showValidationFailure } from '../../ui/message' +import { describeValidationStatus, getValidationStatus, validate } from './stackActionApi' +import { createDeploymentStatusBar, updateWorkflowStatus } from '../../ui/statusBar' +import { commands } from 'vscode' +import { DiffWebviewProvider } from '../../ui/diffWebviewProvider' +import { createValidationParams } from './stackActionUtil' +import { extractErrorMessage } from '../../utils' +import { getLogger } from '../../../../shared/logger/logger' +import { commandKey } from '../../utils' + +// TODO move this to server side, we should let server handle last validation +let lastValidation: Validation | undefined = undefined + +export function getLastValidation(): Validation | undefined { + return lastValidation +} + +export function setLastValidation(validation: Validation | undefined): void { + lastValidation = validation +} + +export class Validation { + private readonly id: string + private status: StackActionPhase | undefined + private changes: StackChange[] | undefined + private statusBarHandle?: { update(phase: StackActionPhase): void; release(): void } + private changeSetName?: string + + constructor( + public readonly uri: string, + public readonly stackName: string, + private readonly client: LanguageClient, + private readonly diffProvider: DiffWebviewProvider, + public readonly parameters?: Parameter[], + private readonly capabilities?: Capability[], + private readonly resourcesToImport?: ResourceToImport[], + private readonly shouldEnableDeployment: boolean = false, + private readonly optionalFlags?: ChangeSetOptionalFlags, + private readonly s3Bucket?: string, + private readonly s3Key?: string + ) { + this.id = uuidv4() + } + + async validate() { + try { + showValidationStarted(this.stackName) + this.statusBarHandle = createDeploymentStatusBar(this.stackName, 'Validation') + // Capture the result to get changeSetName + const result = await validate( + this.client, + createValidationParams( + this.id, + this.uri, + this.stackName, + this.parameters, + this.capabilities, + this.resourcesToImport, + this.shouldEnableDeployment, + this.optionalFlags, + this.s3Bucket, + this.s3Key + ) + ) + + void commands.executeCommand(commandKey('stacks.refresh')) + this.changeSetName = result.changeSetName + + this.pollForProgress() + } catch (error) { + showErrorMessage(`Error validating template: ${error instanceof Error ? error.message : String(error)}`) + } + } + + private pollForProgress() { + const interval = setInterval(() => { + getValidationStatus(this.client, { id: this.id }) + .then(async (validationResult) => { + if (validationResult.phase === this.status) { + return + } + + this.status = validationResult.phase + this.changes = validationResult.changes + + if (this.statusBarHandle) { + updateWorkflowStatus(this.statusBarHandle, validationResult.phase) + } + + switch (validationResult.phase) { + case StackActionPhase.VALIDATION_IN_PROGRESS: + // Status bar updated above + break + case StackActionPhase.VALIDATION_COMPLETE: { + const describeValidationStatusResult = await describeValidationStatus(this.client, { + id: this.id, + }) + if (validationResult.state === StackActionState.SUCCESSFUL) { + showValidationSuccess(this.stackName) + + this.showDiffView( + describeValidationStatusResult.ValidationDetails, + describeValidationStatusResult.deploymentMode + ) + } else { + showValidationFailure( + this.stackName, + describeValidationStatusResult.FailureReason ?? 'UNKNOWN' + ) + } + void commands.executeCommand(commandKey('stacks.refresh')) + this.statusBarHandle?.release() + clearInterval(interval) + break + } + case StackActionPhase.VALIDATION_FAILED: { + const describeValidationStatusResult = await describeValidationStatus(this.client, { + id: this.id, + }) + showValidationFailure( + this.stackName, + describeValidationStatusResult.FailureReason ?? 'UNKNOWN' + ) + void commands.executeCommand('workbench.panel.markers.view.focus') + void commands.executeCommand(commandKey('stacks.refresh')) + this.statusBarHandle?.release() + clearInterval(interval) + break + } + } + }) + .catch((error) => { + getLogger().error(`Error polling for deployment status: ${error}`) + showErrorMessage(`Error polling for validation status: ${extractErrorMessage(error)}`) + void commands.executeCommand(commandKey('stacks.refresh')) + this.statusBarHandle?.release() + clearInterval(interval) + }) + }, 1000) + } + + private showDiffView(validationDetail?: ValidationDetail[], deploymentMode?: DeploymentMode) { + void this.diffProvider.updateData( + this.stackName, + this.changes, + this.changeSetName, + this.shouldEnableDeployment, + validationDetail, + deploymentMode, + ChangeSetStatus.CREATE_COMPLETE + ) + void commands.executeCommand(commandKey('diff.focus')) + } +} diff --git a/packages/core/src/awsService/cloudformation/stacks/changeSetsManager.ts b/packages/core/src/awsService/cloudformation/stacks/changeSetsManager.ts new file mode 100644 index 00000000000..7aaf75bf48f --- /dev/null +++ b/packages/core/src/awsService/cloudformation/stacks/changeSetsManager.ts @@ -0,0 +1,57 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LanguageClient } from 'vscode-languageclient/node' +import { ListChangeSetsRequest } from './actions/stackActionProtocol' +import { ChangeSetInfo } from './actions/stackActionRequestType' + +type StackChangeSets = { + changeSets: ChangeSetInfo[] + nextToken?: string +} + +export class ChangeSetsManager { + private stackChangeSets = new Map() + + constructor(private readonly client: LanguageClient) {} + + async getChangeSets(stackName: string): Promise { + const response = await this.client.sendRequest(ListChangeSetsRequest, { + stackName, + }) + + this.stackChangeSets.set(stackName, { + changeSets: response.changeSets, + nextToken: response.nextToken, + }) + + return response.changeSets + } + + async loadMoreChangeSets(stackName: string): Promise { + const current = this.stackChangeSets.get(stackName) + if (!current?.nextToken) { + return + } + + const response = await this.client.sendRequest(ListChangeSetsRequest, { + stackName, + nextToken: current.nextToken, + }) + + this.stackChangeSets.set(stackName, { + changeSets: [...current.changeSets, ...response.changeSets], + nextToken: response.nextToken, + }) + } + + get(stackName: string): ChangeSetInfo[] { + return this.stackChangeSets.get(stackName)?.changeSets ?? [] + } + + hasMore(stackName: string): boolean { + return this.stackChangeSets.get(stackName)?.nextToken !== undefined + } +} diff --git a/packages/core/src/awsService/cloudformation/stacks/stacksManager.ts b/packages/core/src/awsService/cloudformation/stacks/stacksManager.ts new file mode 100644 index 00000000000..360e799f801 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/stacks/stacksManager.ts @@ -0,0 +1,134 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { commands, Disposable } from 'vscode' +import { StackStatus, StackSummary } from '@aws-sdk/client-cloudformation' +import { RequestType } from 'vscode-languageserver-protocol' +import { LanguageClient } from 'vscode-languageclient/node' +import { handleLspError } from '../utils/onlineErrorHandler' +import { commandKey } from '../utils' +import { setContext } from '../../../shared/vscode/setContext' + +type ListStacksParams = { + statusToInclude?: StackStatus[] + statusToExclude?: StackStatus[] + loadMore?: boolean +} + +type ListStacksResult = { + stacks: StackSummary[] + nextToken?: string +} + +const ListStacksRequest = new RequestType('aws/cfn/stacks') + +type StacksChangeListener = (stacks: StackSummary[]) => void + +export class StacksManager implements Disposable { + private stacks: StackSummary[] = [] + private nextToken?: string + private readonly listeners: StacksChangeListener[] = [] + private loaded = false + + constructor(private readonly client: LanguageClient) {} + + addListener(listener: StacksChangeListener) { + this.listeners.push(listener) + } + + get() { + return [...this.stacks] + } + + hasMore(): boolean { + return this.nextToken !== undefined + } + + reload() { + void this.loadStacks() + } + + isLoaded() { + return this.loaded + } + + async ensureLoaded() { + if (!this.loaded) { + await this.loadStacks() + } + } + + clear() { + this.stacks = [] + this.nextToken = undefined + this.loaded = false + this.notifyListeners() + } + + updateStackStatus(stackName: string, stackStatus: string) { + const stack = this.stacks.find((s) => s.StackName === stackName) + if (stack) { + stack.StackStatus = stackStatus as any + this.notifyListeners() + } + } + + async loadMoreStacks() { + if (!this.nextToken) { + return + } + + await setContext('aws.cloudformation.loadingStacks', true) + try { + const response = await this.client.sendRequest(ListStacksRequest, { + statusToExclude: ['DELETE_COMPLETE'], + loadMore: true, + }) + this.stacks = response.stacks + this.nextToken = response.nextToken + } catch (error) { + await handleLspError(error, 'Error loading more stacks') + } finally { + await setContext('aws.cloudformation.loadingStacks', false) + this.notifyListeners() + } + } + + dispose() { + // do nothing + } + + private async loadStacks() { + await setContext('aws.cloudformation.refreshingStacks', true) + try { + const response = await this.client.sendRequest(ListStacksRequest, { + statusToExclude: ['DELETE_COMPLETE'], + loadMore: false, + }) + this.stacks = response.stacks + this.nextToken = response.nextToken + this.loaded = true + } catch (error) { + await handleLspError(error, 'Error loading stacks') + this.stacks = [] + this.nextToken = undefined + } finally { + await setContext('aws.cloudformation.refreshingStacks', false) + this.notifyListeners() + } + } + + private notifyListeners() { + for (const listener of this.listeners) { + listener(this.stacks) + } + } +} + +export function refreshCommand(manager: StacksManager) { + return commands.registerCommand(commandKey('stacks.refresh'), () => { + manager.reload() + }) +} diff --git a/packages/core/src/awsService/cloudformation/telemetryOptIn.ts b/packages/core/src/awsService/cloudformation/telemetryOptIn.ts new file mode 100644 index 00000000000..0b1f927e30b --- /dev/null +++ b/packages/core/src/awsService/cloudformation/telemetryOptIn.ts @@ -0,0 +1,171 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ExtensionContext, env, Uri, window } from 'vscode' +import { CloudFormationTelemetrySettings } from './extensionConfig' +import { commandKey } from './utils' +import { isAutomation } from '../../shared/vscode/env' +import { getLogger } from '../../shared/logger/logger' +import globals from '../../shared/extensionGlobals' + +enum TelemetryChoice { + Allow = 'Yes, Allow', + Later = 'Not Now', + Never = 'Never', + LearnMore = 'Learn More', +} + +const telemetryKeys = { + hasResponded: commandKey('telemetry.hasResponded'), + lastPromptDate: commandKey('telemetry.lastPromptDate'), + unpersistedResponse: commandKey('telemetry.unpersistedResponse'), +} as const + +const telemetrySettings = { + enabled: 'enabled', +} as const + +const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000 +const promptTimeoutMs = 2500 +const telemetryDocsUrl = 'https://github.com/aws-cloudformation/cloudformation-languageserver/tree/main/src/telemetry' + +/* eslint-disable aws-toolkits/no-banned-usages */ +export async function handleTelemetryOptIn( + context: ExtensionContext, + cfnTelemetrySettings: CloudFormationTelemetrySettings +): Promise { + // If previous choice failed to persist, persist it now and return + const unpersistedResponse = (await context.globalState.get(telemetryKeys.unpersistedResponse)) as string + const hasResponded = context.globalState.get(telemetryKeys.hasResponded) + const lastPromptDate = context.globalState.get(telemetryKeys.lastPromptDate) + if (unpersistedResponse) { + // May still raise popup if user lacks permission or file is corrupted + const didSave = await saveTelemetryResponse(unpersistedResponse, cfnTelemetrySettings) + await context.globalState.update(telemetryKeys.unpersistedResponse, undefined) + // If we still couldn't save, clear everything so they get asked again until the file/perms is fixed + if (!didSave) { + getLogger().warn( + 'CloudFormation telemetry choice was not saved successfully after restart. Clearing related globalState keys for next restart' + ) + await context.globalState.update(telemetryKeys.hasResponded, undefined) + await context.globalState.update(telemetryKeys.lastPromptDate, undefined) + } + return logAndReturnTelemetryChoice( + unpersistedResponse === TelemetryChoice.Allow.toString(), + hasResponded, + lastPromptDate + ) + } + + // Never throws because we provide a default + const telemetryEnabled = cfnTelemetrySettings.get(telemetrySettings.enabled, false) + + if (isAutomation()) { + return logAndReturnTelemetryChoice(telemetryEnabled) + } + + // If user has permanently responded, use their choice + if (hasResponded) { + return logAndReturnTelemetryChoice(telemetryEnabled, hasResponded) + } + + // Check if we should show reminder (30 days since last prompt) + const shouldPrompt = lastPromptDate === undefined || globals.clock.Date.now() - lastPromptDate >= thirtyDaysMs + if (!shouldPrompt) { + return logAndReturnTelemetryChoice(telemetryEnabled, hasResponded, lastPromptDate) + } + + // Show prompt but set false if timeout + const promptPromise = promptTelemetryOptIn(context, cfnTelemetrySettings) + const timeoutPromise = new Promise((resolve) => + globals.clock.setTimeout(() => resolve(false), promptTimeoutMs) + ) + const result = await Promise.race([promptPromise, timeoutPromise]) + + // Keep prompt alive in background + void promptPromise + + return logAndReturnTelemetryChoice(result) +} +/** + * Updates the telemetry setting. In case of error, the update calls do not throw. + * They instead raise a popup and return false. + * + * @returns boolean whether the save/update was successful + */ +/* eslint-disable aws-toolkits/no-banned-usages */ +async function saveTelemetryResponse( + response: string | undefined, + cfnTelemetrySettings: CloudFormationTelemetrySettings +): Promise { + if (response === TelemetryChoice.Allow) { + return await cfnTelemetrySettings.update(telemetrySettings.enabled, true) + } else if (response === TelemetryChoice.Never) { + return await cfnTelemetrySettings.update(telemetrySettings.enabled, false) + } else if (response === TelemetryChoice.Later) { + return await cfnTelemetrySettings.update(telemetrySettings.enabled, false) + } + return false +} + +function logAndReturnTelemetryChoice(choice: boolean, hasResponded?: boolean, lastPromptDate?: number): boolean { + getLogger().info( + 'CloudFormation telemetry: choice=%s, hasResponded=%s, lastPromptDate=%s', + choice, + hasResponded, + lastPromptDate + ) + return choice +} + +/* eslint-disable aws-toolkits/no-banned-usages */ +async function promptTelemetryOptIn( + context: ExtensionContext, + cfnTelemetrySettings: CloudFormationTelemetrySettings +): Promise { + const message = + 'Help us improve the AWS CloudFormation Language Server by sharing anonymous telemetry data with AWS. You can change this preference at any time in aws.cloudformation Settings.' + + const response = await window.showInformationMessage( + message, + TelemetryChoice.Allow, + TelemetryChoice.Later, + TelemetryChoice.Never, + TelemetryChoice.LearnMore + ) + + if (response === TelemetryChoice.LearnMore) { + await env.openExternal(Uri.parse(telemetryDocsUrl)) + return promptTelemetryOptIn(context, cfnTelemetrySettings) + } + + const now = globals.clock.Date.now() + await context.globalState.update(telemetryKeys.lastPromptDate, now) + + // There's a chance our settings aren't registered yet from package.json, so we + // see if we can persist to settings first + try { + // Throws (with no popup) if setting is not registered + cfnTelemetrySettings.get(telemetrySettings.enabled) + } catch (err) { + getLogger().warn(err as Error) + // Save the choice in globalState and save to settings next time handleTelemetryOptIn is called + await context.globalState.update(telemetryKeys.unpersistedResponse, response) + if (response === TelemetryChoice.Allow) { + await context.globalState.update(telemetryKeys.hasResponded, true) + return true + } else if (response === TelemetryChoice.Never) { + await context.globalState.update(telemetryKeys.hasResponded, true) + return false + } else if (response === TelemetryChoice.Later) { + return false + } + } + + // At this point should be able to save and get successfully + await saveTelemetryResponse(response, cfnTelemetrySettings) + await context.globalState.update(telemetryKeys.hasResponded, response !== TelemetryChoice.Later) + return cfnTelemetrySettings.get(telemetrySettings.enabled, false) +} diff --git a/packages/core/src/awsService/cloudformation/ui/cfnEnvironmentFileSelector.ts b/packages/core/src/awsService/cloudformation/ui/cfnEnvironmentFileSelector.ts new file mode 100644 index 00000000000..ec1c52a5973 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/cfnEnvironmentFileSelector.ts @@ -0,0 +1,51 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { window } from 'vscode' +import { CfnEnvironmentFileSelectorItem } from '../cfn-init/cfnProjectTypes' + +export class CfnEnvironmentFileSelector { + public async selectEnvironmentFile( + files: CfnEnvironmentFileSelectorItem[], + requiredParameterCount: number + ): Promise { + // Sort files: matching template path first, then by compatible parameter count (descending) + const sortedFiles = files.sort((a, b) => { + // First sort by hasMatchingTemplatePath (true first) + if (a.hasMatchingTemplatePath !== b.hasMatchingTemplatePath) { + return a.hasMatchingTemplatePath ? -1 : 1 + } + + // Then sort by compatible parameter count (higher first) + const aCount = a.compatibleParameters?.length ?? 0 + const bCount = b.compatibleParameters?.length ?? 0 + return bCount - aCount + }) + + const items = [ + { + label: 'Enter options manually', + detail: 'Skip environment file selection', + parameters: undefined, + }, + ...sortedFiles.map((file) => { + const compatibleCount = file.compatibleParameters?.length ?? 0 + const countText = `${compatibleCount}/${requiredParameterCount} parameters match` + + return { + label: file.hasMatchingTemplatePath ? `$(star-full) ${file.fileName}` : file.fileName, + detail: file.hasMatchingTemplatePath ? `Matching template path • ${countText}` : countText, + parameters: file, + } + }), + ] + + const selected = await window.showQuickPick(items, { + placeHolder: 'Select an environment file or enter manually', + }) + + return selected?.parameters + } +} diff --git a/packages/core/src/awsService/cloudformation/ui/cfnEnvironmentSelector.ts b/packages/core/src/awsService/cloudformation/ui/cfnEnvironmentSelector.ts new file mode 100644 index 00000000000..08a050b30ad --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/cfnEnvironmentSelector.ts @@ -0,0 +1,36 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { commands, window } from 'vscode' +import { CfnEnvironmentConfig, CfnEnvironmentLookup, unselectedValue } from '../cfn-init/cfnProjectTypes' +import { commandKey } from '../utils' + +export class CfnEnvironmentSelector { + public async selectEnvironment(environmentLookup: CfnEnvironmentLookup): Promise { + if (Object.keys(environmentLookup).length === 0) { + const choice = await window.showWarningMessage('No environments found in CFN Project', 'Add environment') + + if (choice === 'Add environment') { + void commands.executeCommand(commandKey('init.addEnvironment')) + } + + return + } + + const items = [ + { label: unselectedValue, description: 'Unselect environment' }, + ...Object.values(environmentLookup).map((env: CfnEnvironmentConfig) => ({ + label: env.name, + description: `AWS Profile: ${env.profile}`, + })), + ] + + const selected = await window.showQuickPick(items, { + placeHolder: 'Select an environment', + }) + + return selected?.label + } +} diff --git a/packages/core/src/awsService/cloudformation/ui/diffViewHelper.ts b/packages/core/src/awsService/cloudformation/ui/diffViewHelper.ts new file mode 100644 index 00000000000..97948465046 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/diffViewHelper.ts @@ -0,0 +1,258 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Uri, commands, workspace, Range, Position, window, ThemeColor } from 'vscode' +import { StackChange } from '../stacks/actions/stackActionRequestType' +import * as path from 'path' +import { fs } from '../../../shared/fs/fs' +import * as os from 'os' + +export class DiffViewHelper { + static async openDiff(stackName: string, changes: StackChange[], resourceId?: string) { + const tmpDir = os.tmpdir() + const beforePath = path.join(tmpDir, `${stackName}-before.json`) + const afterPath = path.join(tmpDir, `${stackName}-after.json`) + + const beforeData: Record = {} + const afterData: Record = {} + + for (const change of changes) { + const rc = change.resourceChange + if (!rc?.logicalResourceId) { + continue + } + + const id = rc.logicalResourceId + + if (rc.action !== 'Add' || rc.resourceDriftStatus === 'DELETED') { + if (rc.beforeContext) { + try { + beforeData[id] = JSON.parse(rc.beforeContext) as Record + } catch { + beforeData[id] = {} + } + } else { + beforeData[id] = {} + } + } + + if (rc.action !== 'Remove') { + if (rc.afterContext) { + try { + afterData[id] = JSON.parse(rc.afterContext) as Record + } catch { + afterData[id] = {} + } + } else { + afterData[id] = {} + } + } + + if (!rc.beforeContext && !rc.afterContext) { + if (rc.details) { + for (const detail of rc.details) { + const target = detail.Target + if (target?.Name) { + if (rc.action !== 'Add') { + ;(beforeData[id] as Record)[target.Name] = + target.BeforeValue ?? '' + } + if (rc.action !== 'Remove') { + ;(afterData[id] as Record)[target.Name] = + target.AfterValue ?? '' + } + } + } + } + } + } + + await fs.writeFile(beforePath, JSON.stringify(beforeData, undefined, 2)) + await fs.writeFile(afterPath, JSON.stringify(afterData, undefined, 2)) + + const beforeUri = Uri.file(beforePath) + const afterUri = Uri.file(afterPath) + + await commands.executeCommand('vscode.diff', beforeUri, afterUri, `${stackName}: Before ↔ After`) + + this.addDriftDecorations(beforeUri, changes) + + if (resourceId) { + // Find the line with the resource ID in the after doc. + // In a deleted resource case this will just be the top + const editor = await workspace.openTextDocument(afterUri) + const text = editor.getText() + const lines = text.split('\n') + const lineIndex = lines.findIndex((line) => line.includes(`"${resourceId}"`)) + + if (lineIndex !== -1) { + await commands.executeCommand('vscode.diff', beforeUri, afterUri, `${stackName}: Before ↔ After`, { + selection: new Range(new Position(lineIndex, 0), new Position(lineIndex + 1, 0)), + }) + } + } + } + + private static propertyExistsInContext(context: string, path: string): boolean { + try { + const data = JSON.parse(context) + const pathParts = path.split('/').filter(Boolean) + let current: any = data + + for (const part of pathParts) { + if (/^\d+$/.test(part)) { + const index = parseInt(part, 10) + if (Array.isArray(current) && current[index] !== undefined) { + current = current[index] + } else { + return false + } + } else if (current && typeof current === 'object' && part in current) { + current = current[part] + } else { + return false + } + } + return true + } catch { + return false + } + } + + private static findPropertyLineIndex(lines: string[], startLineIndex: number, path: string): number { + const pathParts = path.split('/').filter(Boolean) + let currentLineIndex = startLineIndex + + for (const part of pathParts) { + // Skip numeric array indices - they don't appear as keys in JSON + if (/^\d+$/.test(part)) { + continue + } + + const foundIndex = lines.findIndex((line, idx) => idx > currentLineIndex && line.includes(`"${part}"`)) + if (foundIndex < 0) { + return -1 + } + currentLineIndex = foundIndex + } + + return currentLineIndex + } + + private static createDeletedResourceHoverMessage(logicalResourceId: string): string { + return [ + '### ⚠️ Resource Drift Detected', + '', + `**Resource:** \`${logicalResourceId}\``, + '', + '**Status:** Resource Deleted', + '', + '*This resource was deleted sometime after the previous deployment (out-of-band).*', + ].join('\n') + } + + private static createPropertyDriftHoverMessage( + logicalResourceId: string, + path: string, + previousValue: string, + actualValue: string + ): string { + return [ + '### ⚠️ Resource Drift Detected', + '', + `**Resource:** \`${logicalResourceId}\``, + '', + `**Property:** \`${path}\``, + '', + '| Source | Value |', + '|--------|-------|', + `| 📄 Template | \`${previousValue}\` |`, + `| ☁️ Live AWS | \`${actualValue}\` |`, + '', + '*The live resource has drifted from the previously deployed template.*', + ].join('\n') + } + + private static addDriftDecorations(beforeUri: Uri, changes: StackChange[]) { + const driftDecorationType = window.createTextEditorDecorationType({ + after: { + contentText: ' ⚠️ Drifted', + color: new ThemeColor('editorWarning.foreground'), + fontWeight: 'bold', + }, + backgroundColor: new ThemeColor('editorWarning.background'), + cursor: 'pointer', + }) + + setTimeout(() => { + const editors = window.visibleTextEditors.filter( + (editor) => editor.document.uri.toString() === beforeUri.toString() + ) + + for (const editor of editors) { + const decorations: any[] = [] + const lines = editor.document.getText().split('\n') + + for (const change of changes) { + const rc = change.resourceChange + if (!rc?.logicalResourceId) { + continue + } + + const resourceLineIndex = lines.findIndex((line) => line.includes(`"${rc.logicalResourceId}"`)) + if (resourceLineIndex < 0) { + continue + } + + // Handle DELETED drift status + if (rc.resourceDriftStatus === 'DELETED') { + const line = lines[resourceLineIndex] + const endCol = line.trimEnd().length + const range = new Range(resourceLineIndex, endCol, resourceLineIndex, endCol) + const hoverMessage = this.createDeletedResourceHoverMessage(rc.logicalResourceId) + + decorations.push({ range, hoverMessage }) + continue + } + + if (!rc.details) { + continue + } + + for (const detail of rc.details) { + const target = detail.Target + const drift = target?.Drift || target?.LiveResourceDrift + if (drift && target?.Path && drift.ActualValue !== undefined) { + // Check if property exists in afterContext + if (rc.afterContext && !this.propertyExistsInContext(rc.afterContext, target.Path)) { + continue + } + + const currentLineIndex = this.findPropertyLineIndex(lines, resourceLineIndex, target.Path) + if (currentLineIndex <= resourceLineIndex) { + continue + } + + const line = lines[currentLineIndex] + const endCol = line.trimEnd().length + // sets hover range to just the decoration + const range = new Range(currentLineIndex, endCol, currentLineIndex, endCol) + const hoverMessage = this.createPropertyDriftHoverMessage( + rc.logicalResourceId, + target.Path, + drift.PreviousValue, + drift.ActualValue + ) + + decorations.push({ range, hoverMessage }) + } + } + } + + editor.setDecorations(driftDecorationType, decorations) + } + }, 100) + } +} diff --git a/packages/core/src/awsService/cloudformation/ui/diffWebviewProvider.ts b/packages/core/src/awsService/cloudformation/ui/diffWebviewProvider.ts new file mode 100644 index 00000000000..f7c06f5f6f8 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/diffWebviewProvider.ts @@ -0,0 +1,501 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WebviewView, WebviewViewProvider, commands, Disposable } from 'vscode' +import { DeploymentMode, StackChange, ValidationDetail } from '../stacks/actions/stackActionRequestType' +import { DiffViewHelper } from './diffViewHelper' +import { commandKey } from '../utils' +import { StackViewCoordinator } from './stackViewCoordinator' +import { showWarningConfirmation } from './message' +import { ChangeSetStatus } from '@aws-sdk/client-cloudformation' + +const webviewCommandOpenDiff = 'openDiff' + +export class DiffWebviewProvider implements WebviewViewProvider, Disposable { + private _view?: WebviewView + private stackName = '' + private changes: StackChange[] = [] + private changeSetName?: string + private enableDeployments: boolean = false + private currentPage: number = 0 + private pageSize: number = 50 + private totalPages: number = 0 + private readonly disposables: Disposable[] = [] + private validationDetail: ValidationDetail[] = [] + private deploymentMode?: DeploymentMode + private changeSetStatus?: string + + constructor(private readonly coordinator: StackViewCoordinator) { + this.disposables.push( + coordinator.onDidChangeStack((state) => { + if (!state.isChangeSetMode) { + this.stackName = '' + this.changes = [] + this.changeSetName = undefined + if (this._view) { + this._view.webview.html = this.getHtmlContent() + } + } + }) + ) + } + + async updateData( + stackName: string, + changes: StackChange[] = [], + changeSetName?: string, + enableDeployments = false, + validationDetail?: ValidationDetail[], + deploymentMode?: DeploymentMode, + changeSetStatus?: string + ) { + this.stackName = stackName + this.changes = changes + this.changeSetName = changeSetName + this.enableDeployments = enableDeployments + this.currentPage = 0 + this.totalPages = Math.ceil(changes.length / this.pageSize) + if (validationDetail) { + this.validationDetail = validationDetail + } + this.deploymentMode = deploymentMode + this.changeSetStatus = changeSetStatus + + await this.coordinator.setChangeSetMode(stackName, true) + if (this._view) { + this._view.webview.html = this.getHtmlContent() + } + } + + resolveWebviewView(webviewView: WebviewView) { + this._view = webviewView + webviewView.webview.options = { enableScripts: true } + webviewView.webview.html = this.getHtmlContent() + + webviewView.webview.onDidReceiveMessage(async (message: { command: string; resourceId?: string }) => { + if (message.command === webviewCommandOpenDiff) { + void DiffViewHelper.openDiff(this.stackName, this.changes, message.resourceId) + } else if (message.command === 'confirmDeploy') { + if (this.changeSetName) { + const errorCount = this.getErrorCount() + const warningCount = this.getWarningCount() + + if (errorCount === 0 && warningCount > 0) { + const proceed = await showWarningConfirmation(warningCount) + if (!proceed) { + return + } + } + + void commands.executeCommand(commandKey('api.executeChangeSet'), this.stackName, this.changeSetName) + this.changeSetName = undefined + this.enableDeployments = false + this._view!.webview.html = this.getHtmlContent() + } + } else if (message.command === 'deleteChangeSet') { + void commands.executeCommand(commandKey('stacks.deleteChangeSet'), { + stackName: this.stackName, + changeSetName: this.changeSetName, + }) + this.changeSetName = undefined + this.enableDeployments = false + this._view!.webview.html = this.getHtmlContent() + } else if (message.command === 'nextPage') { + if (this.currentPage < this.totalPages - 1) { + this.currentPage++ + this._view!.webview.html = this.getHtmlContent() + } + } else if (message.command === 'prevPage') { + if (this.currentPage > 0) { + this.currentPage-- + this._view!.webview.html = this.getHtmlContent() + } + } + }) + } + + private getHtmlContent(): string { + const changes = this.changes + + const startIndex = this.currentPage * this.pageSize + const endIndex = startIndex + this.pageSize + const displayedChanges = changes.slice(startIndex, endIndex) + const hasNext = this.currentPage < this.totalPages - 1 + const hasPrev = this.currentPage > 0 + const terminalChangeSetStatuses: string[] = [ + ChangeSetStatus.CREATE_COMPLETE, + ChangeSetStatus.FAILED, + ChangeSetStatus.DELETE_FAILED, + ] + + const deletionButton = ` + + ` + + if (!changes || changes.length === 0) { + return ` + + + + + + +

No changes detected for stack: ${this.stackName}

+ ${ + this.changeSetName && + this.changeSetStatus && + terminalChangeSetStatuses.includes(this.changeSetStatus) + ? ` +
+ ${deletionButton} +
+ + ` + : '' + } + + + ` + } + + // Check if REVERT_DRIFT change set or any resource has drift + // TODO: adapt if we do real backend pagination + // TODO: remove resource fallback once server is passing deploymentMode + const hasDrift = + this.deploymentMode === DeploymentMode.REVERT_DRIFT || + changes.some( + (change) => + change.resourceChange?.resourceDriftStatus || + change.resourceChange?.details?.some( + (detail) => detail.Target?.Drift || detail.Target?.LiveResourceDrift + ) + ) + + let tableHtml = ` + + + + + + + + ${ + hasDrift + ? ` + ` + : '' + } + ` + + for (const [changeIndex, change] of displayedChanges.entries()) { + const rc = change.resourceChange + if (!rc) { + continue + } + + const borderColor = + rc.action === 'Add' + ? 'var(--vscode-gitDecoration-addedResourceForeground)' + : rc.action === 'Remove' + ? 'var(--vscode-gitDecoration-deletedResourceForeground)' + : rc.action === 'Modify' + ? 'var(--vscode-gitDecoration-modifiedResourceForeground)' + : 'transparent' + + const hasDetails = rc.details && rc.details.length > 0 + const expandIcon = hasDetails + ? '' + : '' + + const driftStatus = rc.resourceDriftStatus + const hasDriftDetails = rc.details?.some( + (detail) => detail.Target?.Drift || detail.Target?.LiveResourceDrift + ) + let driftDisplay = '' + if (driftStatus === 'DELETED') { + driftDisplay = '⚠️ Deleted' + } else if (hasDriftDetails) { + driftDisplay = '⚠️ Modified' + } else if (driftStatus && driftStatus !== 'IN_SYNC') { + driftDisplay = `⚠️ ${driftStatus}` + } + + tableHtml += ` + + + + + + ${ + hasDrift + ? ` + ` + : '' + } + ` + + if (hasDetails) { + tableHtml += ` + ` + } + } + + tableHtml += `
ActionLogicalResourceIdPhysicalResourceIdResourceTypeReplacementDrift Status
+ ${expandIcon} + ${rc.action ?? 'Unknown'}${rc.logicalResourceId ?? 'Unknown'}${rc.physicalResourceId ?? ' '}${rc.resourceType ?? 'Unknown'}${rc.replacement ?? 'N/A'}${driftDisplay || '-'}
` + + const paginationControls = + this.totalPages > 1 + ? ` +
+ Page ${this.currentPage + 1} of ${this.totalPages} + + +
+ ` + : '' + + const warningBanner = + this.getWarningCount() > 0 + ? ` +
+ ⚠️ ${this.getWarningCount()} warning(s) found +
+ ` + : '' + + const viewDiffButton = ` +
+ +
+ ` + + const deploymentButtons = + this.changeSetName && + this.enableDeployments && + this.changeSetStatus && + terminalChangeSetStatuses.includes(this.changeSetStatus) + ? ` +
+ ${ + this.changeSetStatus === ChangeSetStatus.CREATE_COMPLETE + ? ` + ` + : '' + } + ${deletionButton} +
+ ` + : '' + + return ` + + + + + + + ${warningBanner} + ${paginationControls} +
+ ${viewDiffButton}${deploymentButtons} + ${tableHtml} +
+ + + + ` + } + + private getWarningCount(): number { + return this.validationDetail.filter((detail) => detail.Severity === 'INFO').length + } + + private getErrorCount(): number { + return this.validationDetail.filter((detail) => detail.Severity === 'ERROR').length + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose() + } + } +} diff --git a/packages/core/src/awsService/cloudformation/ui/htmlPreview.ts b/packages/core/src/awsService/cloudformation/ui/htmlPreview.ts new file mode 100644 index 00000000000..500596d0ca6 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/htmlPreview.ts @@ -0,0 +1,20 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ViewColumn } from 'vscode' +import { docPreview } from '../documents/documentPreview' + +export async function htmlPreview(content: unknown, title: string) { + if (typeof content !== 'string') { + return + } + + await docPreview({ + content: `# ${title}\n${content}`, + language: 'markdown', + viewColumn: ViewColumn.Beside, + preserveFocus: true, + }) +} diff --git a/packages/core/src/awsService/cloudformation/ui/inputBox.ts b/packages/core/src/awsService/cloudformation/ui/inputBox.ts new file mode 100644 index 00000000000..cbe7eb2444c --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/inputBox.ts @@ -0,0 +1,629 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { window, workspace, Uri, commands } from 'vscode' +import { + validateStackName, + validateParameterValue, + validateChangeSetName, +} from '../stacks/actions/stackActionInputValidation' +import { Parameter, Capability, Tag, OnStackFailure, Stack, StackStatus } from '@aws-sdk/client-cloudformation' +import { + TemplateParameter, + ResourceToImport, + TemplateResource, + OptionalFlagMode, + DeploymentMode, +} from '../stacks/actions/stackActionRequestType' +import { DocumentManager } from '../documents/documentManager' +import path from 'path' +import fs from '../../../shared/fs/fs' +import { unselectedValue } from '../cfn-init/cfnProjectTypes' + +export async function getTemplatePath(documentManager: DocumentManager): Promise { + const validTemplates = documentManager + .get() + .filter((doc) => doc.cfnType === 'template') + .map((doc) => { + const uri = doc.uri + + return { + label: doc.fileName, + description: workspace.asRelativePath(Uri.parse(uri)), + uri: uri, + } + }) + .sort((a, b) => a.label.localeCompare(b.label)) + + const options = [ + ...validTemplates, + { + label: '$(file) Browse for template file...', + description: 'Select a CloudFormation template file', + uri: 'browse', + }, + ] + + const selected = await window.showQuickPick(options, { + placeHolder: 'Select CloudFormation template', + ignoreFocusOut: true, + }) + + if (!selected) { + return undefined + } + + if (selected.uri === 'browse') { + const fileUri = await window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + filters: { + 'CloudFormation Templates': ['yaml', 'yml', 'json', 'template', 'cfn', 'txt', ''], + }, + title: 'Select CloudFormation Template', + }) + + return fileUri?.[0]?.fsPath + } + + return selected.uri +} + +export async function getStackName(prefill?: string): Promise { + return await window.showInputBox({ + prompt: 'Enter the CloudFormation stack name', + value: prefill, + validateInput: validateStackName, + ignoreFocusOut: true, + }) +} + +export async function getChangeSetName(prefill?: string): Promise { + return await window.showInputBox({ + prompt: 'Enter the CloudFormation change set name', + value: prefill, + validateInput: validateChangeSetName, + ignoreFocusOut: true, + }) +} + +export async function getParameterValues( + templateParameters: TemplateParameter[], + prefillParameters?: Parameter[] +): Promise { + const parameters: Parameter[] = [] + + for (const param of templateParameters) { + const prefillCandidate = prefillParameters?.find((p) => p.ParameterKey === param.name)?.ParameterValue + + // If we are using a previous parameter value, we must ensure that it is compatible with possibly modified template + const prefillValue = + prefillCandidate && !validateParameterValue(prefillCandidate, param) ? prefillCandidate : undefined + + const value = await getParameterValue(param, prefillValue) + if (value) { + parameters.push(value) + } + } + + return parameters +} + +async function getParameterValue(parameter: TemplateParameter, prefill?: string): Promise { + const prompt = `Enter value for parameter "${parameter.name}"${parameter.Description ? ` - ${parameter.Description}` : ''}` + const placeHolder = parameter.Default ? `Default: ${parameter.Default}` : (parameter.Type ?? 'String') + const allowedInfo = parameter.AllowedValues ? ` (Allowed: ${parameter.AllowedValues.join(', ')})` : '' + + const value = await window.showInputBox({ + prompt: prompt + allowedInfo, + placeHolder, + value: prefill ?? parameter.Default?.toString(), + validateInput: (input: string) => validateParameterValue(input, parameter), + ignoreFocusOut: true, + }) + + if (value === undefined) { + return undefined + } + + return { ParameterKey: parameter.name, ParameterValue: value } +} + +export async function confirmCapabilities(capabilities: Capability[]): Promise { + // Confirm if user wants to use detected capabilities + const useDetected = await window.showQuickPick(['Yes', 'No, modify capabilities'], { + placeHolder: `Proceed with detected capabilities: ${capabilities.join(', ') || '(none)'}?`, + canPickMany: false, + }) + + if (!useDetected) { + return undefined // User cancelled + } + + if (useDetected === 'Yes') { + return capabilities + } + + // Allow user to modify capabilities + const allCapabilities = new Map([ + [Capability.CAPABILITY_IAM, 'Allows deployment to create IAM resources'], + [Capability.CAPABILITY_NAMED_IAM, 'Allows deployment to create named IAM resources'], + [Capability.CAPABILITY_AUTO_EXPAND, 'Allows deployment to create resources using macros'], + ]) + + const selected = await window.showQuickPick( + Array.from(allCapabilities.entries()).map(([cap, description]) => ({ + label: cap, + description: description, + picked: capabilities.includes(cap), + })), + { + placeHolder: 'Select capabilities to use', + canPickMany: true, + } + ) + + return selected ? selected.map((item) => item.label) : undefined +} + +export async function shouldImportResources(): Promise { + const choice = await window.showQuickPick(['Deploy new/updated resources', 'Import existing resources'], { + placeHolder: 'Select deployment mode', + ignoreFocusOut: true, + }) + + return choice === 'Import existing resources' +} + +export async function chooseOptionalFlagSuggestion(): Promise { + const choice = await window.showQuickPick( + [ + { + label: OptionalFlagMode.Input, + }, + { + label: OptionalFlagMode.DevFriendly, + }, + { + label: OptionalFlagMode.Skip, + description: 'Skip optional flags', + }, + ], + { + placeHolder: 'Enter optional change set flags?', + ignoreFocusOut: true, + } + ) + + return choice?.label +} + +export async function getTags(previousTags?: Tag[]): Promise { + const prefill = previousTags + ?.filter((tag) => tag.Key && tag.Value) + .map((tag) => `${tag.Key}=${tag.Value}`) + .join(',') + + const input = await window.showInputBox({ + prompt: 'Enter CloudFormation tags (key=value pairs, comma-separated). Enter empty for no tags', + placeHolder: 'key1=value1,key2=value2,key3=value3', + value: prefill, + validateInput: (value) => { + if (!value) { + return undefined + } + const isValid = /^[^=,]+=[^=,]+(,[^=,]+=[^=,]+)*$/.test(value.trim()) + return isValid ? undefined : 'Format: key1=value1,key2=value2' + }, + ignoreFocusOut: true, + }) + + if (!input) { + return undefined + } + + return input.split(',').map((pair) => { + const [key, value] = pair.split('=').map((s) => s.trim()) + return { Key: key, Value: value } + }) +} + +export async function getIncludeNestedStacks(): Promise { + return ( + await window.showQuickPick( + [ + { label: 'Yes', value: true }, + { label: 'No', value: false }, + ], + { placeHolder: 'Include nested stacks?', ignoreFocusOut: true } + ) + )?.value +} + +export async function getImportExistingResources(): Promise { + return ( + await window.showQuickPick( + [ + { label: 'Yes', value: true }, + { label: 'No', value: false }, + ], + { placeHolder: 'Import existing resources?', ignoreFocusOut: true } + ) + )?.value +} + +export async function getOnStackFailure(stackDetails?: Stack): Promise { + const options: Array<{ label: string; description: string; value: OnStackFailure }> = [ + { label: 'Do nothing', description: 'Leave stack in failed state', value: OnStackFailure.DO_NOTHING }, + { label: 'Rollback', description: 'Rollback to previous state', value: OnStackFailure.ROLLBACK }, + ] + + if (!stackDetails || stackDetails.StackStatus === StackStatus.REVIEW_IN_PROGRESS) { + // only a valid option for CREATE + options.unshift({ label: 'Delete', description: 'Delete the stack on failure', value: OnStackFailure.DELETE }) + } + + return (await window.showQuickPick(options, { placeHolder: 'What to do on stack failure?', ignoreFocusOut: true })) + ?.value +} + +export async function getDeploymentMode(): Promise { + return ( + await window.showQuickPick( + [ + { + label: 'Revert Drift', + description: 'Revert drift during deployment (disables dev friendly flags)', + value: DeploymentMode.REVERT_DRIFT, + }, + { label: 'Standard', description: 'No special handling during deployment', value: undefined }, + ], + { placeHolder: 'Select deployment mode', ignoreFocusOut: true } + ) + )?.value +} + +export async function getResourcesToImport( + templateResources: TemplateResource[] +): Promise { + const resourcesToImport: ResourceToImport[] = [] + + const selectedResources = await window.showQuickPick( + templateResources.map((r) => ({ + label: r.logicalId, + description: r.type, + picked: false, + resource: r, + })), + { + placeHolder: 'Select resources to import', + canPickMany: true, + ignoreFocusOut: true, + } + ) + + if (!selectedResources || selectedResources.length === 0) { + return undefined + } + + for (const selected of selectedResources) { + const resourceIdentifier = await getResourceIdentifier( + selected.resource.logicalId, + selected.resource.type, + selected.resource.primaryIdentifierKeys, + selected.resource.primaryIdentifier + ) + + if (!resourceIdentifier) { + return undefined + } + + resourcesToImport.push({ + ResourceType: selected.resource.type, + LogicalResourceId: selected.resource.logicalId, + ResourceIdentifier: resourceIdentifier, + }) + } + + return resourcesToImport +} + +async function getResourceIdentifier( + logicalId: string, + resourceType: string, + primaryIdentifierKeys?: string[], + primaryIdentifier?: Record +): Promise | undefined> { + if (!primaryIdentifierKeys || primaryIdentifierKeys.length === 0) { + void window.showErrorMessage(`No primary identifier keys found for ${resourceType}`) + return undefined + } + + if (primaryIdentifier && Object.keys(primaryIdentifier).length > 0) { + const id = Object.values(primaryIdentifier).join('|') + + const usePrimary = await window.showQuickPick([id, 'Enter manually'], { + placeHolder: `Select primary identifier for ${logicalId}`, + ignoreFocusOut: true, + }) + if (!usePrimary) { + return undefined + } + if (usePrimary === id) { + return primaryIdentifier + } + } + + const identifiers: Record = {} + + for (const key of primaryIdentifierKeys) { + const value = await window.showInputBox({ + prompt: `Enter ${key} for ${logicalId} (${resourceType})`, + placeHolder: `Physical ${key} of existing resource`, + ignoreFocusOut: true, + }) + + if (!value) { + return undefined + } + + identifiers[key] = value + } + + return identifiers +} + +export async function getProjectName(prefillValue: string | undefined) { + return await window.showInputBox({ + prompt: 'Enter project name', + value: prefillValue, + validateInput: (v) => { + if (!v.trim()) { + return 'Required' + } + if (v.trim() === unselectedValue) { + return `Project name cannot be ${unselectedValue}` + } + if (!/^[a-zA-Z0-9_-]{1,64}$/.test(v.trim())) { + return 'Must be 1-64 characters, alphanumeric with hyphens and underscores only' + } + return undefined + }, + }) +} + +export async function getProjectPath(prefillValue: string) { + const selected = await window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + title: 'Select project directory', + defaultUri: prefillValue ? Uri.file(prefillValue) : undefined, + }) + + if (!selected || selected.length === 0) { + return undefined // User cancelled + } + + const input = selected[0].fsPath + + // Validate the selected path + try { + const resolvedPath = path.resolve(input) + const parentDir = path.dirname(resolvedPath) + + const parentPathExists = await fs.existsDir(parentDir) + if (!parentPathExists) { + void window.showErrorMessage('Parent directory does not exist.') + return undefined + } + + // Check if we can write to the directory + try { + await fs.checkPerms(resolvedPath, '*w*') + } catch { + // If directory doesn't exist, check parent directory write permissions + try { + await fs.checkPerms(parentDir, '*w*') + } catch { + void window.showErrorMessage( + 'Cannot write to this location. Please choose a path you have write permissions for.' + ) + return undefined + } + } + + // Check if cfn-project directory already exists + const cfnProjectPath = path.join(resolvedPath, 'cfn-project') + const cfnProjectExists = await fs.existsDir(cfnProjectPath) + if (cfnProjectExists) { + void window.showErrorMessage( + 'A cfn-project directory already exists at this location. Please choose a different path.' + ) + return undefined + } + + return input + } catch (error) { + void window.showErrorMessage('Invalid path format.') + return undefined + } +} + +export async function getEnvironmentName() { + return await window.showInputBox({ + prompt: 'Environment name', + validateInput: (v) => { + if (!v.trim()) { + return 'Required' + } + if (!/^[a-zA-Z0-9_-]{1,32}$/.test(v.trim())) { + return 'Must be 1-32 characters, alphanumeric with hyphens and underscores only' + } + return undefined + }, + }) +} + +export async function shouldSaveFlagsToFile(): Promise { + const config = workspace.getConfiguration('aws.cloudformation') + const currentSetting = config.get('environment.saveOptions', 'alwaysAsk') + + if (currentSetting === 'alwaysSave') { + return true + } + if (currentSetting === 'neverSave') { + return false + } + + const choice = await window.showQuickPick( + [ + { + label: 'Save options to file', + description: 'Save the deployment options to a file in your environment', + value: 'save', + }, + { + label: 'Configure in settings', + description: + 'Open CloudFormation Environment settings (settings will not affect this current deployment)', + value: 'configure', + }, + { + label: 'Skip for now', + description: 'Do not save options to environment file', + value: 'skip', + }, + ], + { + placeHolder: 'Choose deployment options configuration for CloudFormation template', + ignoreFocusOut: true, + } + ) + + if (!choice) { + return false + } + + if (choice.value === 'configure') { + await commands.executeCommand('workbench.action.openSettings', 'aws.cloudformation.environment.saveOptions') + return undefined // Exit command, let user configure first + } + + return choice.value === 'save' +} + +export async function getFilePath(environmentDir: string) { + while (true) { + const input = await window.showInputBox({ + prompt: 'Enter File Name to save options to (must be .json, .yaml, or .yml)', + ignoreFocusOut: true, + validateInput: (v) => { + if (!v.trim()) { + return 'Required' + } + if (!/^[a-zA-Z0-9_-]{1,32}\.(json|yaml|yml)$/.test(v.trim())) { + return 'Must be 1-32 characters (alphanumeric with hyphens and underscores) and end with .json, .yaml, or .yml' + } + return undefined + }, + }) + + if (input === undefined) { + return undefined + } // User cancelled + + // Validate after input + try { + const resolvedPath = path.resolve(path.join(environmentDir, input.trim())) + + const parentPathExists = await fs.existsFile(resolvedPath) + if (parentPathExists) { + void window.showErrorMessage('File already exists. Please try again.') + continue // Ask again + } + + return resolvedPath + } catch (error) { + void window.showErrorMessage('Environment directory was not found') + return + } + } +} + +export async function shouldUploadToS3(): Promise { + const config = workspace.getConfiguration('aws.cloudformation') + const currentSetting = config.get('s3', 'alwaysAsk') + + if (currentSetting === 'alwaysUpload') { + return true + } + if (currentSetting === 'neverUpload') { + return false + } + + const choice = await window.showQuickPick( + [ + { + label: 'Upload to S3', + description: 'Upload template to S3', + value: 'upload', + }, + { + label: 'Do not upload to S3', + description: 'Do not upload template to S3', + value: 'skip', + }, + { + label: 'Configure in Settings', + description: 'Open CloudFormation S3 settings', + value: 'configure', + }, + ], + { + placeHolder: 'Choose S3 upload option for CloudFormation template', + } + ) + + if (!choice) { + return false + } + + if (choice.value === 'configure') { + await commands.executeCommand('workbench.action.openSettings', 'aws.cloudformation.s3') + return undefined // Exit command, let user configure first + } + + return choice.value === 'upload' +} + +export async function getS3Bucket(prompt?: string): Promise { + return await window.showInputBox({ + prompt: prompt || 'Enter S3 bucket name', + validateInput: (value) => { + if (!value.trim()) { + return 'Bucket name is required' + } + if (!/^[a-z0-9.-]{3,63}$/.test(value)) { + return 'Invalid bucket name format' + } + return undefined + }, + }) +} + +export async function getS3Key(prefill?: string): Promise { + return await window.showInputBox({ + prompt: 'Enter S3 object key', + value: prefill, + validateInput: (value) => { + if (!value.trim()) { + return 'Object key is required' + } + return undefined + }, + }) +} diff --git a/packages/core/src/awsService/cloudformation/ui/message.ts b/packages/core/src/awsService/cloudformation/ui/message.ts new file mode 100644 index 00000000000..55056002522 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/message.ts @@ -0,0 +1,89 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { window } from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node' +import { getDeploymentStatus } from '../stacks/actions/stackActionApi' +import { StackActionPhase, StackActionState } from '../stacks/actions/stackActionRequestType' + +export async function showDeploymentCompletion( + client: LanguageClient, + deploymentId: string, + stackName: string +): Promise { + try { + const pollResult = await getDeploymentStatus(client, { id: deploymentId }) + + if ( + pollResult.phase === StackActionPhase.DEPLOYMENT_COMPLETE && + pollResult.state === StackActionState.SUCCESSFUL + ) { + void window.showInformationMessage(`Deployment completed successfully for stack: ${stackName}`) + } else if ( + pollResult.phase === StackActionPhase.DEPLOYMENT_FAILED || + pollResult.phase === StackActionPhase.VALIDATION_FAILED || + pollResult.state === StackActionState.FAILED + ) { + void window.showErrorMessage(`Deployment failed for stack: ${stackName}`) + } else { + void window.showWarningMessage(`Deployment status unknown for stack: ${stackName}`) + } + } catch (error) { + void window.showErrorMessage(`Error checking deployment status for stack: ${stackName}`) + } +} + +export function showDeploymentSuccess(stackName: string) { + void window.showInformationMessage(`Deployment completed successfully for stack: ${stackName}`) +} + +export function showChangeSetDeletionSuccess(changeSetName: string, stackName: string) { + void window.showInformationMessage( + `Deletion completed successfully for change set: ${changeSetName}, in stack: ${stackName}` + ) +} + +export function showDeploymentFailure(stackName: string, failureReason: string) { + void window.showErrorMessage(`Deployment failed for stack: ${stackName} with reason: ${failureReason}`) +} + +export function showChangeSetDeletionFailure(changeSetName: string, stackName: string, failureReason: string) { + void window.showErrorMessage( + `Change Set Deletion failed for change set: ${changeSetName}, in stack: ${stackName} with reason: ${failureReason}` + ) +} + +export function showValidationStarted(stackName: string) { + void window.showInformationMessage(`Validation started for stack: ${stackName}`) +} + +export function showValidationSuccess(stackName: string) { + void window.showInformationMessage(`Validation completed successfully for stack: ${stackName}`) +} + +export function showValidationFailure(stackName: string, failureReason: string) { + void window.showErrorMessage(`Validation failed for stack: ${stackName} with reason: ${failureReason}`) +} + +export function showDeploymentStarted(stackName: string) { + void window.showInformationMessage(`Deployment started for stack: ${stackName}`) +} + +export function showChangeSetDeletionStarted(changeSetName: string, stackName: string) { + void window.showInformationMessage(`Deletion started for change set: ${changeSetName}, in stack: ${stackName}`) +} + +export function showErrorMessage(message: string) { + void window.showErrorMessage(message) +} + +export async function showWarningConfirmation(warningCount: number): Promise { + const proceed = await window.showWarningMessage( + `There are ${warningCount} warning(s). Do you want to proceed with deployment?`, + 'Proceed', + 'Cancel' + ) + return proceed === 'Proceed' +} diff --git a/packages/core/src/awsService/cloudformation/ui/relatedResourceSelector.ts b/packages/core/src/awsService/cloudformation/ui/relatedResourceSelector.ts new file mode 100644 index 00000000000..63be2d7c3fb --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/relatedResourceSelector.ts @@ -0,0 +1,52 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { window } from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node' +import { getAuthoredResourceTypes, getRelatedResourceTypes } from '../relatedResources/relatedResourcesApi' + +export class RelatedResourceSelector { + constructor(private client: LanguageClient) {} + + async selectAuthoredResourceType(templateUri: string): Promise { + const resourceTypes = await getAuthoredResourceTypes(this.client, templateUri) + if (resourceTypes.length === 0) { + void window.showInformationMessage('No resources found in the current template') + return undefined + } + + return window.showQuickPick(resourceTypes, { + placeHolder: 'Select an existing resource type from your template', + canPickMany: false, + }) + } + + async promptCreateOrImport(): Promise<'create' | 'import' | undefined> { + const action = await window.showQuickPick(['Create new', 'Import existing'], { + placeHolder: 'How would you like to add related resource types?', + canPickMany: false, + }) + + if (!action) { + return undefined + } + + return action === 'Create new' ? 'create' : 'import' + } + + async selectRelatedResourceTypes(selectedResourceType: string): Promise { + const relatedTypes = await getRelatedResourceTypes(this.client, { parentResourceType: selectedResourceType }) + + if (relatedTypes.length === 0) { + void window.showInformationMessage(`No related resources found for ${selectedResourceType}`) + return undefined + } + + return window.showQuickPick(relatedTypes, { + placeHolder: 'Select related resource types', + canPickMany: true, + }) + } +} diff --git a/packages/core/src/awsService/cloudformation/ui/resourceSelector.ts b/packages/core/src/awsService/cloudformation/ui/resourceSelector.ts new file mode 100644 index 00000000000..9444896a489 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/resourceSelector.ts @@ -0,0 +1,202 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { window } from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node' +import { + ResourceTypesRequest, + ListResourcesRequest, + ResourceList, + SearchResourceResult, +} from '../resources/resourceRequestTypes' +import { handleLspError } from '../utils/onlineErrorHandler' +import { getLogger } from '../../../shared/logger/logger' + +export interface ResourceSelectionResult { + resourceType: string + resourceIdentifier: string +} + +export interface ResourceOperations { + getCached: (resourceType: string) => ResourceList | undefined + loadMore: (resourceType: string, nextToken: string) => Promise + search: (resourceType: string, identifier: string) => Promise +} + +export class ResourceSelector { + public refreshCallback?: () => void + + constructor(private client: LanguageClient) {} + + setRefreshCallback(callback: () => void): void { + this.refreshCallback = callback + } + + async selectResourceTypes(selectedTypes: string[] = [], multiSelect = true): Promise { + try { + const response = await this.client.sendRequest(ResourceTypesRequest, {}) + const availableTypes = response.resourceTypes + + if (availableTypes.length === 0) { + void window.showWarningMessage('No resource types available') + return undefined + } + + const quickPickItems = availableTypes.map((type: string) => ({ + label: type, + picked: selectedTypes.includes(type), + })) + + const result = await window.showQuickPick(quickPickItems, { + canPickMany: multiSelect, + placeHolder: 'Select resource types', + title: 'Select Resource Types', + }) + + if (!result) { + return undefined + } + + if (Array.isArray(result)) { + return result.map((item: { label: string }) => item.label) + } + return [(result as { label: string }).label] + } catch (error) { + getLogger().error(`Failed to get resource types: ${error}`) + void window.showErrorMessage('Failed to get available resource types') + return undefined + } + } + + async selectResources( + multiSelect = true, + preSelectedTypes?: string[], + resourceOperations?: ResourceOperations + ): Promise { + try { + let selectedTypes: string[] + + if (preSelectedTypes && preSelectedTypes.length > 0) { + selectedTypes = preSelectedTypes + } else { + const types = await this.selectResourceTypes([], multiSelect) + if (!types || types.length === 0) { + return [] + } + selectedTypes = types + } + + const allSelections: ResourceSelectionResult[] = [] + + for (const resourceType of selectedTypes) { + const selection = await this.selectResourcesForType(resourceType, multiSelect, resourceOperations) + allSelections.push(...selection) + } + + return allSelections + } catch (error) { + await handleLspError(error, 'Error selecting resources') + return [] + } + } + + private async selectResourcesForType( + resourceType: string, + multiSelect: boolean, + resourceOperations?: ResourceOperations + ): Promise { + let resourceList = resourceOperations?.getCached(resourceType) + + if (!resourceList) { + resourceList = await this.fetchResourceList(resourceType) + } + + if (!resourceList || resourceList.resourceIdentifiers.length === 0) { + void window.showWarningMessage(`No resources found for type: ${resourceType}`) + return [] + } + + while (resourceList.nextToken && resourceOperations) { + const action = await this.showLoadMoreMenu(resourceType, resourceList.resourceIdentifiers.length) + + if (action === 'load') { + await resourceOperations.loadMore(resourceType, resourceList.nextToken) + this.refreshCallback?.() + const updatedList = resourceOperations.getCached(resourceType) + if (!updatedList) { + break + } + resourceList = updatedList + } else if (action === 'search') { + const identifier = await window.showInputBox({ + prompt: `Enter ${resourceType} identifier`, + placeHolder: 'Resource identifier must match exactly', + }) + + if (!identifier) { + return [] + } + + const result = await resourceOperations.search(resourceType, identifier) + this.refreshCallback?.() + + if (!result.found) { + void window.showErrorMessage( + `${resourceType} with identifier '${identifier}' was not found. The identifier must match exactly.` + ) + return [] + } + + return [{ resourceType, resourceIdentifier: identifier }] + } else if (action === 'select') { + break + } else { + return [] + } + } + + const result = await window.showQuickPick(resourceList.resourceIdentifiers, { + canPickMany: multiSelect, + placeHolder: `Select ${resourceType} identifiers`, + title: `Select ${resourceType} Resources`, + }) + + if (!result) { + return [] + } + + const identifiers = Array.isArray(result) ? result : [result] + return identifiers.map((identifier) => ({ resourceType, resourceIdentifier: identifier })) + } + + private async showLoadMoreMenu(resourceType: string, loadedCount: number): Promise { + const result = await window.showQuickPick( + [ + { label: `Load more resources (${loadedCount} currently loaded)`, value: 'load' }, + { label: 'Search by identifier', value: 'search' }, + { label: `Select from loaded resources (${loadedCount} available)`, value: 'select' }, + ], + { + placeHolder: `Choose how to select ${resourceType} resources`, + title: resourceType, + } + ) + + return result?.value + } + + private async fetchResourceList(resourceType: string): Promise { + const response = await this.client.sendRequest(ListResourcesRequest, { + resources: [{ resourceType }], + }) + + return response.resources.find((r: { typeName: string }) => r.typeName === resourceType) + } + + async selectSingleResource(resourceOperations?: ResourceOperations): Promise { + const result = await this.selectResources(false, undefined, resourceOperations) + return result[0] + } +} diff --git a/packages/core/src/awsService/cloudformation/ui/sectionUI.ts b/packages/core/src/awsService/cloudformation/ui/sectionUI.ts new file mode 100644 index 00000000000..5471ab9e14b --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/sectionUI.ts @@ -0,0 +1,13 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EventEmitter, TreeItem } from 'vscode' + +export interface SectionUI { + base: TreeItem + children(element?: T): (T | null | undefined)[] + registerTreeChangedEvent(event: EventEmitter): void + onChange: () => void +} diff --git a/packages/core/src/awsService/cloudformation/ui/stackEventsWebviewProvider.ts b/packages/core/src/awsService/cloudformation/ui/stackEventsWebviewProvider.ts new file mode 100644 index 00000000000..b17ac79950d --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/stackEventsWebviewProvider.ts @@ -0,0 +1,391 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WebviewView, WebviewViewProvider, Disposable } from 'vscode' +import { StackEvent } from '@aws-sdk/client-cloudformation' +import { LanguageClient } from 'vscode-languageclient/node' +import { extractErrorMessage, getStackStatusClass, isStackInTransientState } from '../utils' +import { GetStackEventsRequest, ClearStackEventsRequest } from '../stacks/actions/stackActionProtocol' +import { StackViewCoordinator } from './stackViewCoordinator' +import { arnToConsoleTabUrl, externalLinkSvg, consoleLinkStyles } from '../consoleLinksUtils' + +const EventsPerPage = 50 +const RefreshIntervalMs = 5000 + +export class StackEventsWebviewProvider implements WebviewViewProvider, Disposable { + private view?: WebviewView + private stackName?: string + private stackArn?: string + private allEvents: StackEvent[] = [] + private currentPage = 0 + private nextToken?: string + private refreshTimer?: NodeJS.Timeout + private readonly disposables: Disposable[] = [] + private readonly coordinatorSubscription: Disposable + + constructor( + private readonly client: LanguageClient, + coordinator: StackViewCoordinator + ) { + this.coordinatorSubscription = coordinator.onDidChangeStack(async (state) => { + try { + if (state.stackName && !state.isChangeSetMode) { + this.stopAutoRefresh() + this.stackArn = state.stackArn + await this.showStackEvents(state.stackName) + } else if (!state.stackName || state.isChangeSetMode) { + this.stopAutoRefresh() + this.stackName = undefined + this.stackArn = undefined + this.allEvents = [] + this.render() + } + + if (state.stackStatus && !isStackInTransientState(state.stackStatus)) { + this.stopAutoRefresh() + } + } catch (error) { + // Silently handle errors to prevent breaking the coordinator + } + }) + } + + async showStackEvents(stackName: string): Promise { + this.stackName = stackName + this.allEvents = [] + this.currentPage = 0 + this.nextToken = undefined + + try { + const result = await this.client.sendRequest(GetStackEventsRequest, { + stackName: this.stackName, + }) + this.allEvents = result.events + this.nextToken = result.nextToken + } catch (error) { + this.renderError(`Failed to load events: ${extractErrorMessage(error)}`) + } + + this.render() + this.startAutoRefresh() + } + + resolveWebviewView(webviewView: WebviewView): void { + this.view = webviewView + webviewView.webview.options = { enableScripts: true } + webviewView.onDidDispose(() => { + this.stopAutoRefresh() + }) + webviewView.onDidChangeVisibility(() => { + if (webviewView.visible) { + this.startAutoRefresh() + } else { + this.stopAutoRefresh() + } + }) + + webviewView.webview.onDidReceiveMessage(async (message: { command: string }) => { + if (message.command === 'nextPage') { + await this.nextPage() + } else if (message.command === 'prevPage') { + await this.prevPage() + } + }) + + this.render() + } + + dispose(): void { + this.stopAutoRefresh() + if (this.stackName) { + void this.client.sendRequest(ClearStackEventsRequest, { stackName: this.stackName }) + } + for (const d of this.disposables) { + d.dispose() + } + this.coordinatorSubscription.dispose() + } + + private async loadEvents(): Promise { + if (!this.stackName || !this.nextToken) { + return + } + + try { + const result = await this.client.sendRequest(GetStackEventsRequest, { + stackName: this.stackName, + nextToken: this.nextToken, + }) + + this.allEvents.push(...result.events) + this.nextToken = result.nextToken + } catch (error) { + this.renderError(`Failed to load events: ${extractErrorMessage(error)}`) + } + } + + private async refresh(): Promise { + if (!this.stackName) { + return + } + + try { + const result = await this.client.sendRequest(GetStackEventsRequest, { + stackName: this.stackName, + refresh: true, + }) + + if (result.gapDetected) { + const initialResult = await this.client.sendRequest(GetStackEventsRequest, { + stackName: this.stackName, + }) + this.allEvents = initialResult.events + this.nextToken = initialResult.nextToken + this.currentPage = 0 + this.render('Event history reloaded due to high activity') + } else if (result.events.length > 0) { + this.allEvents.unshift(...result.events) + this.currentPage = 0 + this.render() + } + } catch (error) { + this.renderError(`Failed to refresh events: ${extractErrorMessage(error)}`) + } + } + + private async nextPage(): Promise { + const totalPages = Math.ceil(this.allEvents.length / EventsPerPage) + const nextPageIndex = this.currentPage + 1 + + if (nextPageIndex < totalPages) { + this.currentPage = nextPageIndex + this.render() + } else if (this.nextToken) { + await this.loadEvents() + this.currentPage = nextPageIndex + this.render() + } + } + + private async prevPage(): Promise { + if (this.currentPage > 0) { + this.currentPage-- + this.render() + } else { + await this.refresh() + } + } + + private startAutoRefresh(): void { + this.stopAutoRefresh() + this.refreshTimer = setInterval(() => void this.refresh(), RefreshIntervalMs) + } + + private stopAutoRefresh(): void { + if (this.refreshTimer) { + clearInterval(this.refreshTimer) + this.refreshTimer = undefined + } + } + + private renderError(message: string): void { + if (!this.view || this.view.visible === false) { + return + } + this.view.webview.html = ` + + + + + + +

Error

+

${message}

+ +` + } + + private render(notification?: string): void { + if (!this.view || this.view.visible === false) { + return + } + + const start = this.currentPage * EventsPerPage + const end = start + EventsPerPage + const pageEvents = this.allEvents.slice(start, end) + const totalPages = Math.ceil(this.allEvents.length / EventsPerPage) + const hasMore = this.nextToken !== undefined + + this.view.webview.html = this.getHtml( + pageEvents, + this.currentPage + 1, + totalPages, + hasMore, + this.allEvents.length, + notification + ) + } + + private getHtml( + events: StackEvent[], + currentPage: number, + totalPages: number, + hasMore: boolean, + totalEvents: number, + notification?: string + ): string { + return ` + + + + + + +
+
+
+ ${this.stackName ?? ''} + ${this.stackArn ? `${externalLinkSvg()}` : ''} + (${totalEvents} events${hasMore ? ' loaded' : ''}) +
+ +
+
+ ${notification ? `
${notification}
` : ''} +
+ + + + + + + + + + + + ${events + .map( + (e) => ` + + + + + + + + ` + ) + .join('')} + +
TimestampResourceTypeStatusReason
${e.Timestamp ? new Date(e.Timestamp).toLocaleString() : '-'}${e.LogicalResourceId ?? '-'}${e.ResourceType ?? '-'}${e.ResourceStatus ?? '-'}${e.ResourceStatusReason ?? '-'}
+
+ + +` + } +} diff --git a/packages/core/src/awsService/cloudformation/ui/stackOutputsWebviewProvider.ts b/packages/core/src/awsService/cloudformation/ui/stackOutputsWebviewProvider.ts new file mode 100644 index 00000000000..8c54b5d9067 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/stackOutputsWebviewProvider.ts @@ -0,0 +1,223 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WebviewView, WebviewViewProvider, Disposable } from 'vscode' +import { Output } from '@aws-sdk/client-cloudformation' +import { LanguageClient } from 'vscode-languageclient/node' +import { extractErrorMessage } from '../utils' +import { DescribeStackRequest } from '../stacks/actions/stackActionProtocol' +import { StackViewCoordinator } from './stackViewCoordinator' +import { arnToConsoleTabUrl, externalLinkSvg, consoleLinkStyles } from '../consoleLinksUtils' + +export class StackOutputsWebviewProvider implements WebviewViewProvider, Disposable { + private view?: WebviewView + private stackName?: string + private stackArn?: string + private outputs: Output[] = [] + private readonly disposables: Disposable[] = [] + + constructor( + private readonly client: LanguageClient, + private readonly coordinator: StackViewCoordinator + ) { + this.disposables.push( + coordinator.onDidChangeStack(async (state) => { + if (state.stackName && !state.isChangeSetMode) { + this.stackName = state.stackName + this.stackArn = state.stackArn + this.outputs = [] + this.render() + await this.showOutputs(state.stackName) + } else if (!state.stackName || state.isChangeSetMode) { + this.stackName = undefined + this.stackArn = undefined + this.outputs = [] + this.render() + } + }) + ) + } + + async resolveWebviewView(webviewView: WebviewView): Promise { + this.view = webviewView + webviewView.webview.options = { enableScripts: true } + + if (this.stackName) { + await this.loadOutputs() + } else { + this.render() + } + } + + async showOutputs(stackName: string): Promise { + this.stackName = stackName + this.outputs = [] + + if (this.view) { + await this.loadOutputs() + } + } + + private async loadOutputs(): Promise { + if (!this.stackName) { + return + } + + try { + const result = await this.client.sendRequest(DescribeStackRequest, { + stackName: this.stackName, + }) + + this.outputs = result.stack?.Outputs ?? [] + // Only update coordinator if status changed + if (result.stack?.StackStatus && this.coordinator.currentStackStatus !== result.stack.StackStatus) { + await this.coordinator.setStack(this.stackName, result.stack.StackStatus, result.stack.StackId) + } + this.render() + } catch (error) { + this.renderError(`Failed to load outputs: ${extractErrorMessage(error)}`) + } + } + + private renderError(message: string): void { + if (!this.view || !this.view.visible) { + return + } + this.view.webview.html = ` + + + + + + +

Error

+

${message}

+ +` + } + + private render(): void { + if (!this.view || this.view.visible === false) { + return + } + + this.view.webview.html = this.getHtml(this.outputs) + } + + private getHtml(outputs: Output[]): string { + const outputRows = + outputs.length > 0 + ? outputs + .map( + (output) => ` + + ${output.OutputKey ?? ''} + ${output.OutputValue ?? ''} + ${output.Description ?? ''} + ${output.ExportName ?? ''} + + ` + ) + .join('') + : 'No outputs found' + + return ` + + + + + + +
+
+ ${this.stackName ?? ''} + ${this.stackArn ? `${externalLinkSvg()}` : ''} + (${outputs.length} outputs) +
+
+
+ + + + + + + + + + + ${outputRows} + +
KeyValueDescriptionExport Name
+
+ +` + } + + dispose(): void { + this.view = undefined + for (const d of this.disposables) { + d.dispose() + } + } +} diff --git a/packages/core/src/awsService/cloudformation/ui/stackOverviewWebviewProvider.ts b/packages/core/src/awsService/cloudformation/ui/stackOverviewWebviewProvider.ts new file mode 100644 index 00000000000..d240be89247 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/stackOverviewWebviewProvider.ts @@ -0,0 +1,281 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WebviewView, WebviewViewProvider, Disposable } from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node' +import { Stack } from '@aws-sdk/client-cloudformation' +import { StackViewCoordinator } from './stackViewCoordinator' +import { DescribeStackRequest } from '../stacks/actions/stackActionProtocol' +import { extractErrorMessage, getStackStatusClass, isStackInTransientState } from '../utils' +import { externalLinkSvg, consoleLinkStyles, arnToConsoleUrl } from '../consoleLinksUtils' + +export class StackOverviewWebviewProvider implements WebviewViewProvider, Disposable { + private view?: WebviewView + private stack?: Stack + private readonly disposables: Disposable[] = [] + private refreshTimer?: NodeJS.Timeout + private currentStackName?: string + + constructor( + private readonly client: LanguageClient, + private readonly coordinator: StackViewCoordinator + ) { + this.disposables.push( + coordinator.onDidChangeStack(async (state) => { + if (state.stackName && !state.isChangeSetMode) { + this.stopAutoRefresh() + this.currentStackName = state.stackName + this.stack = undefined + this.render() + await this.loadStack(state.stackName) + this.startAutoRefresh() + } else { + this.stopAutoRefresh() + this.currentStackName = undefined + this.stack = undefined + this.render() + } + + // Stop auto-refresh if stack is in terminal state + if (state.stackStatus && !isStackInTransientState(state.stackStatus)) { + this.stopAutoRefresh() + } + }) + ) + } + + private startAutoRefresh(): void { + this.stopAutoRefresh() + if (this.currentStackName) { + this.refreshTimer = setInterval(() => { + if (this.currentStackName) { + void this.loadStack(this.currentStackName) + } + }, 5000) + } + } + + private stopAutoRefresh(): void { + if (this.refreshTimer) { + clearInterval(this.refreshTimer) + this.refreshTimer = undefined + } + } + + private async loadStack(stackName: string): Promise { + try { + const result = await this.client.sendRequest(DescribeStackRequest, { stackName }) + if (result.stack) { + this.stack = result.stack + // Only update coordinator if status changed + if (this.coordinator.currentStackStatus !== result.stack.StackStatus) { + await this.coordinator.setStack(stackName, result.stack.StackStatus, result.stack.StackId) + } + this.render() + } + } catch (error) { + this.stack = undefined + this.renderError(`Failed to load stack: ${extractErrorMessage(error)}`) + } + } + + resolveWebviewView(webviewView: WebviewView): void { + this.view = webviewView + webviewView.webview.options = { enableScripts: true } + + webviewView.onDidChangeVisibility(() => { + if (webviewView.visible && this.currentStackName) { + this.startAutoRefresh() + } else { + this.stopAutoRefresh() + } + }) + + webviewView.onDidDispose(() => { + this.stopAutoRefresh() + }) + + this.render() + } + + async showStackOverview(stackName: string): Promise { + if (this.view) { + await this.loadStack(stackName) + } + } + + private render(): void { + if (!this.view || !this.view.visible) { + return + } + + if (!this.stack) { + this.view.webview.html = this.getEmptyContent() + return + } + + this.view.webview.html = this.getWebviewContent(this.stack) + } + + private renderError(message: string): void { + if (!this.view || !this.view.visible) { + return + } + this.view.webview.html = ` + + + + + + +

Error

+

${message}

+ +` + } + + private getEmptyContent(): string { + return ` + + + + + + +

Select a stack to view details

+ +` + } + + private getWebviewContent(stack: Stack): string { + return ` + + + + + + +
+
Stack Name
+
+ ${stack.StackName ?? 'N/A'} + ${stack.StackId ? `${externalLinkSvg()}` : ''} +
+
+
+
Status
+
+ ${stack.StackStatus ?? 'UNKNOWN'} +
+
+ ${ + stack.StackId + ? ` +
+
Stack ID
+
${stack.StackId}
+
` + : '' + } + ${ + stack.Description + ? ` +
+
Description
+
${stack.Description}
+
` + : '' + } + ${ + stack.CreationTime + ? ` +
+
Created
+
${new Date(stack.CreationTime).toLocaleString()}
+
` + : '' + } + ${ + stack.LastUpdatedTime + ? ` +
+
Last Updated
+
${new Date(stack.LastUpdatedTime).toLocaleString()}
+
` + : '' + } + ${ + stack.StackStatusReason + ? ` +
+
Status Reason
+
${stack.StackStatusReason}
+
` + : '' + } + +` + } + + dispose(): void { + this.stopAutoRefresh() + for (const d of this.disposables) { + d.dispose() + } + } +} diff --git a/packages/core/src/awsService/cloudformation/ui/stackResourcesWebviewProvider.ts b/packages/core/src/awsService/cloudformation/ui/stackResourcesWebviewProvider.ts new file mode 100644 index 00000000000..5c78f99635f --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/stackResourcesWebviewProvider.ts @@ -0,0 +1,419 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WebviewView, WebviewViewProvider, Disposable } from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node' +import { showErrorMessage } from './message' +import { GetStackResourcesRequest } from '../stacks/actions/stackActionProtocol' +import { StackResourceSummary, GetStackResourcesParams } from '../stacks/actions/stackActionRequestType' +import { StackViewCoordinator } from './stackViewCoordinator' +import { arnToConsoleTabUrl, externalLinkSvg, consoleLinkStyles } from '../consoleLinksUtils' + +const ResourcesPerPage = 50 + +export class StackResourcesWebviewProvider implements WebviewViewProvider, Disposable { + private _view?: WebviewView + private stackName = '' + private stackArn = '' + private allResources: StackResourceSummary[] = [] + private currentPage = 0 + private nextToken?: string + private updateInterval?: NodeJS.Timeout + private readonly disposables: Disposable[] = [] + + constructor( + private client: LanguageClient, + private readonly coordinator: StackViewCoordinator + ) { + this.disposables.push( + coordinator.onDidChangeStack(async (state) => { + if (state.stackName && !state.isChangeSetMode) { + this.stopAutoRefresh() + this.stackName = state.stackName + this.stackArn = state.stackArn || '' + this.allResources = [] + this.currentPage = 0 + this.nextToken = undefined + if (this._view && this._view.visible) { + this._view.webview.html = this.getHtmlContent() + } + await this.updateData(state.stackName) + } else if (!state.stackName || state.isChangeSetMode) { + this.stopAutoRefresh() + this.stackName = '' + this.stackArn = '' + this.allResources = [] + if (this._view && this._view.visible) { + this._view.webview.html = this.getHtmlContent() + } + } + + // Stop auto-refresh if stack is in terminal state + if (state.stackStatus && !this.isStackInTransientState(state.stackStatus)) { + this.stopAutoUpdate() + } + }) + ) + } + + private isStackInTransientState(status: string): boolean { + return status.includes('_IN_PROGRESS') || status.includes('_CLEANUP_IN_PROGRESS') + } + + async updateData(stackName: string) { + this.stackName = stackName + this.allResources = [] + this.currentPage = 0 + this.nextToken = undefined + await this.loadResources() + + if (this._view && this._view.visible) { + this._view.webview.html = this.getHtmlContent() + } + } + + resolveWebviewView(webviewView: WebviewView) { + this._view = webviewView + this.setupWebview(webviewView) + this.setupMessageHandling(webviewView) + this.setupLifecycleHandlers(webviewView) + } + + private setupWebview(webviewView: WebviewView) { + webviewView.webview.options = { enableScripts: true } + webviewView.webview.html = this.getHtmlContent() + } + + private setupMessageHandling(webviewView: WebviewView) { + webviewView.webview.onDidReceiveMessage(async (message: { command: string }) => { + if (message.command === 'nextPage') { + await this.loadNextPage() + } else if (message.command === 'prevPage') { + await this.loadPrevPage() + } + }) + } + + private setupLifecycleHandlers(webviewView: WebviewView) { + webviewView.onDidChangeVisibility(() => { + if (webviewView.visible) { + this.startAutoUpdate() + } else { + this.stopAutoUpdate() + } + }) + + if (webviewView.visible) { + this.startAutoUpdate() + } + + webviewView.onDidDispose(() => { + this.stopAutoUpdate() + }) + } + + private async loadResources(): Promise { + if (!this.client || !this.stackName) { + return + } + + try { + const params: GetStackResourcesParams = { stackName: this.stackName } + if (this.nextToken) { + params.nextToken = this.nextToken + const result = await this.client.sendRequest(GetStackResourcesRequest, params) + this.allResources.push(...result.resources) + this.nextToken = result.nextToken + } else { + const result = await this.client.sendRequest(GetStackResourcesRequest, params) + this.allResources = result.resources + this.nextToken = result.nextToken + } + } catch (error) { + showErrorMessage(`Failed to fetch stack resources: ${error}`) + } + } + + private async loadNextPage(): Promise { + const totalPages = Math.ceil(this.allResources.length / ResourcesPerPage) + const nextPageIndex = this.currentPage + 1 + + // Don't proceed if we're already at the last page and have no more data + if (nextPageIndex >= totalPages && !this.nextToken) { + return + } + + if (nextPageIndex < totalPages) { + this.currentPage = nextPageIndex + this.render() + } else if (this.nextToken) { + await this.loadResources() + if (this.allResources.length > nextPageIndex * ResourcesPerPage) { + this.currentPage = nextPageIndex + this.render() + } + } + } + + private async loadPrevPage(): Promise { + // Don't proceed if we're already at the first page + if (this.currentPage <= 0) { + return + } + + this.currentPage-- + this.render() + } + + private render(): void { + if (this._view && this._view.visible !== false) { + this._view.webview.html = this.getHtmlContent() + } + } + + private startAutoUpdate() { + if (!this.updateInterval && this.stackName) { + this.updateInterval = setInterval(async () => { + if (this._view && !this.coordinator.currentStackStatus?.includes('_IN_PROGRESS')) { + this.stopAutoUpdate() + return + } + + if (this._view) { + // Reset to page 1 with fresh data + this.allResources = [] + this.currentPage = 0 + this.nextToken = undefined + await this.loadResources() + + if (this._view && this._view.visible !== false) { + this._view.webview.html = this.getHtmlContent() + } + } + }, 5000) + } + } + + private stopAutoUpdate() { + if (this.updateInterval) { + clearInterval(this.updateInterval) + this.updateInterval = undefined + } + } + + private stopAutoRefresh() { + this.stopAutoUpdate() + } + + private getHtmlContent(): string { + const start = this.currentPage * ResourcesPerPage + const end = start + ResourcesPerPage + const pageResources = this.allResources.slice(start, end) + const totalPages = Math.ceil(this.allResources.length / ResourcesPerPage) + const hasMore = this.nextToken !== undefined + + if (!pageResources || pageResources.length === 0) { + return ` + + + + + + +
+
+ ${this.stackName} + ${this.stackArn ? `${externalLinkSvg()}` : ''} + (0 resources) +
+
+
+

No resources found

+
+ +` + } + + const resourceRows = pageResources + .map( + (resource) => ` + + ${resource.LogicalResourceId} + ${resource.PhysicalResourceId || ''} + ${resource.ResourceType} + ${resource.ResourceStatus} + + ` + ) + .join('') + + return ` + + + + + + +
+
+
+ ${this.stackName} + ${this.stackArn ? `${externalLinkSvg()}` : ''} + (${this.allResources.length} resources${hasMore ? ' loaded' : ''}) +
+ +
+
+
+ + + + + + + + + + + ${resourceRows} + +
Logical IDPhysical IDTypeStatus
+
+ + +` + } + + dispose(): void { + this.stopAutoRefresh() + for (const d of this.disposables) { + d.dispose() + } + } +} diff --git a/packages/core/src/awsService/cloudformation/ui/stackViewCoordinator.ts b/packages/core/src/awsService/cloudformation/ui/stackViewCoordinator.ts new file mode 100644 index 00000000000..8fc6e9750b9 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/stackViewCoordinator.ts @@ -0,0 +1,94 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EventEmitter } from 'vscode' +import { setContext } from '../../../shared/vscode/setContext' + +export interface StackViewState { + stackName?: string + stackArn?: string + isChangeSetMode: boolean + stackStatus?: string +} + +export class StackViewCoordinator { + private readonly _onDidChangeStack = new EventEmitter() + readonly onDidChangeStack = this._onDidChangeStack.event + + private _currentStackName?: string + private _currentStackArn?: string + private _isChangeSetMode = false + private _currentStackStatus?: string + private _stackStatusUpdateCallback?: (stackName: string, stackStatus: string) => void + + get currentStackName(): string | undefined { + return this._currentStackName + } + + get currentStackArn(): string | undefined { + return this._currentStackArn + } + + get isChangeSetMode(): boolean { + return this._isChangeSetMode + } + + get currentStackStatus(): string | undefined { + return this._currentStackStatus + } + + setStackStatusUpdateCallback(callback: (stackName: string, stackStatus: string) => void): void { + this._stackStatusUpdateCallback = callback + } + + async setStack(stackName: string, stackStatus?: string, stackArn?: string): Promise { + const statusChanged = stackStatus && this._currentStackStatus !== stackStatus + + this._currentStackName = stackName + this._currentStackArn = stackArn + this._currentStackStatus = stackStatus + this._isChangeSetMode = false + await this.updateContexts() + this._onDidChangeStack.fire(this.getState()) + + if (statusChanged && stackStatus && this._stackStatusUpdateCallback) { + this._stackStatusUpdateCallback(stackName, stackStatus) + } + } + + async setChangeSetMode(stackName: string, enabled: boolean): Promise { + this._currentStackName = stackName + this._isChangeSetMode = enabled + await this.updateContexts() + this._onDidChangeStack.fire(this.getState()) + } + + async clearStack(): Promise { + this._currentStackName = undefined + this._currentStackArn = undefined + this._currentStackStatus = undefined + this._isChangeSetMode = false + await this.updateContexts() + this._onDidChangeStack.fire(this.getState()) + } + + private async updateContexts(): Promise { + await setContext('aws.cloudformation.stackSelected', !!this._currentStackName) + await setContext('aws.cloudformation.changeSetMode', this._isChangeSetMode) + } + + private getState(): StackViewState { + return { + stackName: this._currentStackName, + stackArn: this._currentStackArn, + isChangeSetMode: this._isChangeSetMode, + stackStatus: this._currentStackStatus, + } + } + + dispose(): void { + this._onDidChangeStack.dispose() + } +} diff --git a/packages/core/src/awsService/cloudformation/ui/statusBar.ts b/packages/core/src/awsService/cloudformation/ui/statusBar.ts new file mode 100644 index 00000000000..0a6623e56e4 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/ui/statusBar.ts @@ -0,0 +1,265 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { window, StatusBarAlignment, StatusBarItem, ThemeColor, QuickPickItem, commands } from 'vscode' +import { StackActionPhase } from '../stacks/actions/stackActionRequestType' +import { commandKey } from '../utils' + +const OperationTypeValidation = 'Validation' as const +const OperationTypeDeployment = 'Deployment' as const +const StatusBarLabel = 'AWS CloudFormation' +const StatusValidating = 'Validating' +const StatusValidated = 'Validated' +const StatusDeploying = 'Deploying' +const StatusDeployed = 'Deployed' +const StatusValidationFailed = 'Validation Failed' +const StatusDeploymentFailed = 'Deployment Failed' +const StatusProcessing = 'Processing' +const NoOperationsMessage = 'No active operations' + +interface OperationInfo { + stackName: string + type: typeof OperationTypeValidation | typeof OperationTypeDeployment + changeSetName?: string + startTime: Date + phase: StackActionPhase + released: boolean +} + +interface StatusBarHandle { + update(phase: StackActionPhase): void + release(): void +} + +class SharedStatusBar { + private statusBarItem?: StatusBarItem + private refCount = 0 + private activeCount = 0 + private completedCount = 0 + private failedCount = 0 + private disposeTimer?: NodeJS.Timeout + private operations: Map = new Map() + private nextId = 0 + + acquire( + stackName: string, + type: typeof OperationTypeValidation | typeof OperationTypeDeployment, + changeSetName?: string + ): StatusBarHandle { + if (this.disposeTimer) { + clearTimeout(this.disposeTimer) + this.disposeTimer = undefined + this.activeCount = 0 + this.completedCount = 0 + this.failedCount = 0 + } + + if (!this.statusBarItem) { + this.statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 100) + this.statusBarItem.command = commandKey('showOperationStatus') + this.statusBarItem.show() + } + + this.refCount++ + this.activeCount++ + + const id = this.nextId++ + this.operations.set(id, { + stackName, + type, + changeSetName, + startTime: new Date(), + phase: StackActionPhase.VALIDATION_IN_PROGRESS, + released: false, + }) + + this.updateDisplay() + + return { + update: (phase: StackActionPhase) => this.updatePhase(id, phase), + release: () => this.release(id), + } + } + + private updatePhase(id: number, phase: StackActionPhase): void { + const operation = this.operations.get(id) + if (operation) { + operation.phase = phase + } + + if (isTerminalPhase(phase)) { + this.activeCount-- + if (isFailurePhase(phase)) { + this.failedCount++ + } else { + this.completedCount++ + } + } + this.updateDisplay() + } + + private updateDisplay(): void { + if (!this.statusBarItem) { + return + } + + const unreleasedOperations = Array.from(this.operations.values()).filter((op) => !op.released) + const total = unreleasedOperations.length + + if (total === 1) { + const operation = unreleasedOperations[0] + const verb = operation.type === OperationTypeValidation ? StatusValidating : StatusDeploying + const pastVerb = operation.type === OperationTypeValidation ? StatusValidated : StatusDeployed + const isOperationFailed = isFailurePhase(operation.phase) + + if (this.activeCount > 0) { + this.statusBarItem.text = `$(sync~spin) ${verb} ${operation.stackName}` + this.statusBarItem.backgroundColor = undefined + } else if (isOperationFailed) { + const failedStatus = + operation.type === OperationTypeValidation ? StatusValidationFailed : StatusDeploymentFailed + this.statusBarItem.text = `$(error) ${failedStatus}: ${operation.stackName}` + this.statusBarItem.backgroundColor = new ThemeColor('statusBarItem.errorBackground') + } else { + this.statusBarItem.text = `$(check) ${pastVerb} ${operation.stackName}` + this.statusBarItem.backgroundColor = undefined + } + } else if (total > 1) { + if (this.activeCount > 0) { + this.statusBarItem.text = `$(sync~spin) ${StatusBarLabel} (${total})` + this.statusBarItem.backgroundColor = undefined + } else if (this.failedCount > 0) { + this.statusBarItem.text = `$(error) ${StatusBarLabel} (${total})` + this.statusBarItem.backgroundColor = new ThemeColor('statusBarItem.errorBackground') + } else { + this.statusBarItem.text = `$(check) ${StatusBarLabel} (${total})` + this.statusBarItem.backgroundColor = undefined + } + } + } + + private release(id: number): void { + const operation = this.operations.get(id) + if (operation) { + operation.released = true + } + this.refCount-- + + if (this.refCount === 0) { + this.disposeTimer = setTimeout(() => { + this.statusBarItem?.dispose() + this.statusBarItem = undefined + this.activeCount = 0 + this.completedCount = 0 + this.failedCount = 0 + this.operations.clear() + }, 5000) + } + } + + showOperations(): void { + const items: QuickPickItem[] = [] + + for (const operation of this.operations.values()) { + const icon = getPhaseIcon(operation.phase) + const elapsed = formatElapsed(operation.startTime) + const changeSetInfo = operation.changeSetName ? ` • ${operation.changeSetName}` : '' + + items.push({ + label: `${icon} ${operation.stackName}`, + description: `${operation.type}${changeSetInfo}`, + detail: `${getPhaseLabel(operation.phase)} • Started ${elapsed}`, + }) + } + + if (items.length === 0) { + items.push({ + label: `$(info) ${NoOperationsMessage}`, + description: '', + }) + } + + const quickPick = window.createQuickPick() + quickPick.items = items + quickPick.placeholder = `${StatusBarLabel} Operations` + quickPick.canSelectMany = false + quickPick.matchOnDescription = false + quickPick.matchOnDetail = false + quickPick.show() + + quickPick.onDidHide(() => quickPick.dispose()) + } +} + +const sharedStatusBar = new SharedStatusBar() + +function isTerminalPhase(phase: StackActionPhase): boolean { + return [ + StackActionPhase.VALIDATION_COMPLETE, + StackActionPhase.VALIDATION_FAILED, + StackActionPhase.DEPLOYMENT_COMPLETE, + StackActionPhase.DEPLOYMENT_FAILED, + ].includes(phase) +} + +function isFailurePhase(phase: StackActionPhase): boolean { + return [StackActionPhase.VALIDATION_FAILED, StackActionPhase.DEPLOYMENT_FAILED].includes(phase) +} + +function getPhaseIcon(phase: StackActionPhase): string { + if (isFailurePhase(phase)) { + return '$(error)' + } + if (isTerminalPhase(phase)) { + return '$(check)' + } + return '$(sync~spin)' +} + +function getPhaseLabel(phase: StackActionPhase): string { + switch (phase) { + case StackActionPhase.VALIDATION_IN_PROGRESS: + return StatusValidating + case StackActionPhase.VALIDATION_COMPLETE: + return StatusValidated + case StackActionPhase.VALIDATION_FAILED: + return StatusValidationFailed + case StackActionPhase.DEPLOYMENT_IN_PROGRESS: + return StatusDeploying + case StackActionPhase.DEPLOYMENT_COMPLETE: + return StatusDeployed + case StackActionPhase.DEPLOYMENT_FAILED: + return StatusDeploymentFailed + default: + return StatusProcessing + } +} + +function formatElapsed(startTime: Date): string { + const elapsed = Math.floor((Date.now() - startTime.getTime()) / 1000) + if (elapsed < 60) { + return `${elapsed}s ago` + } + const minutes = Math.floor(elapsed / 60) + return `${minutes}m ago` +} + +export function createDeploymentStatusBar( + stackName: string, + type: typeof OperationTypeValidation | typeof OperationTypeDeployment, + changeSetName?: string +): StatusBarHandle { + return sharedStatusBar.acquire(stackName, type, changeSetName) +} + +export function updateWorkflowStatus(handle: StatusBarHandle, status: StackActionPhase): void { + handle.update(status) +} + +export function registerStatusBarCommand(): void { + commands.registerCommand(commandKey('showOperationStatus'), () => { + sharedStatusBar.showOperations() + }) +} diff --git a/packages/core/src/awsService/cloudformation/utils.ts b/packages/core/src/awsService/cloudformation/utils.ts new file mode 100644 index 00000000000..214f1f4b6e8 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/utils.ts @@ -0,0 +1,153 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ExtensionConfigKey, ExtensionId } from './extensionConfig' +import { Position } from 'vscode' + +export function toString(value: unknown): string { + if (value === undefined || !['object', 'function'].includes(typeof value)) { + return String(value) + } + + return JSON.stringify(value) +} + +export function formatMessage(message: string): string { + return `${ExtensionId}: ${message}` +} + +export function commandKey(key: string): string { + return `${ExtensionConfigKey}.${key}` +} + +export const cloudFormationUiClickMetric = 'cloudformation_nodeExpansion' + +export function getStackStatusClass(status?: string): string { + if (!status) { + return '' + } + // Terminal success states + if (status.includes('COMPLETE') && !status.includes('ROLLBACK')) { + return 'status-complete' + } + // Terminal failed states + if (status.includes('FAILED') || status.includes('ROLLBACK')) { + return 'status-failed' + } + // Transient states (in progress) + if (status.includes('PROGRESS')) { + return 'status-progress' + } + return '' +} + +export function isStackInTransientState(status: string): boolean { + return status.includes('_IN_PROGRESS') || status.includes('_CLEANUP_IN_PROGRESS') +} + +export function extractErrorMessage(error: unknown) { + if (error instanceof Error) { + const prefix = error.name === 'Error' ? '' : `${error.name}: ` + return `${prefix}${error.message}` + } + + return toString(error) +} + +/** + * Finds the position of the parameter description value where the cursor should be placed. + * Returns the position between the quotes of the Description property. + */ +export function findParameterDescriptionPosition( + text: string, + parameterName: string, + documentType: string +): Position | undefined { + const lines = text.split('\n') + + if (documentType === 'JSON') { + return findJsonParameterDescriptionPosition(lines, parameterName) + } else { + return findYamlParameterDescriptionPosition(lines, parameterName) + } +} + +/** + * Finds the description position in JSON format. + * Looks for: "ParameterName": { ... "Description": "HERE" ... } + */ +function findParameterDescription( + lines: string[], + parameterPattern: RegExp, + descriptionMatcher: (line: string) => { match: RegExpMatchArray; character: number } | undefined, + endMatcher: (line: string) => boolean +): Position | undefined { + let inParameter = false + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + + if (!inParameter && parameterPattern.test(line)) { + inParameter = true + continue + } + + if (inParameter) { + const result = descriptionMatcher(line) + if (result) { + return new Position(i, result.character) + } + + if (endMatcher(line)) { + break + } + } + } + + return undefined +} + +function findJsonParameterDescriptionPosition(lines: string[], parameterName: string): Position | undefined { + const parameterPattern = new RegExp(`^\\s*"${escapeRegex(parameterName)}"\\s*:\\s*\\{`) + + return findParameterDescription( + lines, + parameterPattern, + (line) => { + const match = line.match(/^(\s*)"Description"\s*:\s*"([^"]*)"/) + return match + ? { match, character: match[1].length + '"Description": "'.length + match[2].length } + : undefined + }, + (line) => !!line.match(/^\s*\}/) + ) +} + +/** + * Finds the description position in YAML format. + * Looks for: ParameterName: ... Description: "HERE" ... + */ +function findYamlParameterDescriptionPosition(lines: string[], parameterName: string): Position | undefined { + const parameterPattern = new RegExp(`^\\s*${escapeRegex(parameterName)}\\s*:`) + + return findParameterDescription( + lines, + parameterPattern, + (line) => { + const match = line.match(/^(\s*)Description\s*:\s*(['"]?)([^'"]*)\2/) + return match + ? { match, character: match[1].length + 'Description: '.length + match[2].length + match[3].length } + : undefined + }, + (line) => !!line.match(/^\s*\w+\s*:/) && !line.match(/^\s*(Type|Default|Description|AllowedValues)\s*:/) + ) +} + +/** + * Escapes special regex characters in a string. + */ +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} diff --git a/packages/core/src/awsService/cloudformation/utils/onlineErrorHandler.ts b/packages/core/src/awsService/cloudformation/utils/onlineErrorHandler.ts new file mode 100644 index 00000000000..292ad7bef14 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/utils/onlineErrorHandler.ts @@ -0,0 +1,58 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { commands, window } from 'vscode' +import { ResponseError } from 'vscode-languageclient' +import { extractErrorMessage } from '../utils' + +// LSP error codes from server (must match OnlineFeatureErrorCode enum) +const OnlineFeatureErrorCode = { + NoInternet: -32_001, + NoAuthentication: -32_002, + ExpiredCredentials: -32_003, + AwsServiceError: -32_004, +} as const + +interface OnlineFeatureErrorData { + retryable: boolean + requiresReauth: boolean +} + +function isLspError(error: unknown): error is ResponseError { + return error instanceof ResponseError +} + +export async function handleLspError(error: unknown, context?: string): Promise { + if (!isLspError(error)) { + const message = context ? `${context}: ${extractErrorMessage(error)}` : extractErrorMessage(error) + void window.showErrorMessage(message) + return + } + + const { code, message, data } = error + const fullMessage = context ? `${context}: ${message}` : message + + switch (code) { + case OnlineFeatureErrorCode.ExpiredCredentials: + case OnlineFeatureErrorCode.NoAuthentication: + if (data?.requiresReauth) { + const action = await window.showErrorMessage(fullMessage, 'Re-authenticate') + if (action === 'Re-authenticate') { + await commands.executeCommand('aws.toolkit.login') + } + } else { + void window.showErrorMessage(fullMessage) + } + break + + case OnlineFeatureErrorCode.NoInternet: + case OnlineFeatureErrorCode.AwsServiceError: + void window.showErrorMessage(fullMessage) + break + + default: + void window.showErrorMessage(fullMessage) + } +} diff --git a/packages/core/src/awsService/ec2/utils.ts b/packages/core/src/awsService/ec2/utils.ts index 5ce24debaca..a86f358eab9 100644 --- a/packages/core/src/awsService/ec2/utils.ts +++ b/packages/core/src/awsService/ec2/utils.ts @@ -7,7 +7,7 @@ import { Ec2Instance } from '../../shared/clients/ec2' import { copyToClipboard } from '../../shared/utilities/messages' import { Ec2Selection } from './prompter' import { sshLogFileLocation } from '../../shared/sshConfig' -import { SSM } from 'aws-sdk' +import { StartSessionResponse } from '@aws-sdk/client-ssm' import { getLogger } from '../../shared/logger/logger' export function getIconCode(instance: Ec2Instance) { @@ -33,7 +33,7 @@ export async function copyInstanceId(instanceId: string): Promise { export function getEc2SsmEnv( selection: Ec2Selection, ssmPath: string, - session: SSM.StartSessionResponse + session: StartSessionResponse ): NodeJS.ProcessEnv { return Object.assign( { diff --git a/packages/core/src/awsService/ecs/model.ts b/packages/core/src/awsService/ecs/model.ts index eba6b51fcf4..57f49576e25 100644 --- a/packages/core/src/awsService/ecs/model.ts +++ b/packages/core/src/awsService/ecs/model.ts @@ -7,7 +7,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() import * as vscode from 'vscode' -import { ECS } from 'aws-sdk' +import { Cluster as SdkCluster, ContainerDefinition, Service as SdkService, Task } from '@aws-sdk/client-ecs' import { DefaultEcsClient } from '../../shared/clients/ecsClient' import { ResourceTreeNode } from '../../shared/treeview/resource' import { getIcon } from '../../shared/icons' @@ -15,7 +15,7 @@ import { AsyncCollection } from '../../shared/utilities/asyncCollection' import { prepareCommand } from './util' function createValidTaskFilter(containerName: string) { - return function (t: ECS.Task): t is ECS.Task & { taskArn: string } { + return function (t: Task): t is Task & { taskArn: string } { const managed = !!t.containers?.find( (c) => c?.name === containerName && c.managedAgents?.find((a) => a.name === 'ExecuteCommandAgent') ) @@ -24,7 +24,7 @@ function createValidTaskFilter(containerName: string) { } } -interface ContainerDescription extends ECS.ContainerDefinition { +interface ContainerDescription extends ContainerDefinition { readonly clusterArn: string readonly taskRoleArn: string readonly enableExecuteCommand?: boolean @@ -82,7 +82,7 @@ export class Service { public constructor( private readonly client: DefaultEcsClient, - public readonly description: ECS.Service + public readonly description: SdkService ) {} public async listContainers(): Promise { @@ -154,7 +154,7 @@ export class Cluster { public constructor( private readonly client: DefaultEcsClient, - private readonly cluster: ECS.Cluster + private readonly cluster: SdkCluster ) {} public listServices(): AsyncCollection { diff --git a/packages/core/src/awsService/ecs/util.ts b/packages/core/src/awsService/ecs/util.ts index d8567373bc6..c25c2291048 100644 --- a/packages/core/src/awsService/ecs/util.ts +++ b/packages/core/src/awsService/ecs/util.ts @@ -13,9 +13,9 @@ import { IamClient } from '../../shared/clients/iam' import { ToolkitError } from '../../shared/errors' import { isCloud9 } from '../../shared/extensionUtilities' import { getOrInstallCli } from '../../shared/utilities/cliUtils' -import { Session, TaskDefinition } from 'aws-sdk/clients/ecs' +import { Session, TaskDefinition } from '@aws-sdk/client-ecs' import { getLogger } from '../../shared/logger/logger' -import { SSM } from 'aws-sdk' +import { SSMClient, TerminateSessionCommand } from '@aws-sdk/client-ssm' import { fromExtensionManifest } from '../../shared/settings' import { ecsTaskPermissionsUrl } from '../../shared/constants' @@ -93,12 +93,13 @@ export async function prepareCommand( async function terminateSession() { const sessionId = session.sessionId! - const ssm = await globals.sdkClientBuilder.createAwsService(SSM, undefined, client.regionCode) - ssm.terminateSession({ SessionId: sessionId }) - .promise() - .catch((err) => { - getLogger().warn(`ecs: failed to terminate session "${sessionId}": %s`, err) - }) + const ssm = globals.sdkClientBuilderV3.createAwsService({ + serviceClient: SSMClient, + clientOptions: { region: client.regionCode }, + }) + ssm.send(new TerminateSessionCommand({ SessionId: sessionId })).catch((err) => { + getLogger().warn(`ecs: failed to terminate session "${sessionId}": %s`, err) + }) } return { path: ssmPlugin, args, dispose: () => void terminateSession() } diff --git a/packages/core/src/awsService/iot/commands/attachCertificate.ts b/packages/core/src/awsService/iot/commands/attachCertificate.ts index be9edd03d89..684160dafbf 100644 --- a/packages/core/src/awsService/iot/commands/attachCertificate.ts +++ b/packages/core/src/awsService/iot/commands/attachCertificate.ts @@ -12,7 +12,7 @@ import { createQuickPick, DataQuickPickItem } from '../../../shared/ui/pickerPro import { PromptResult } from '../../../shared/ui/prompter' import { IotClient } from '../../../shared/clients/iotClient' import { isValidResponse } from '../../../shared/wizards/wizard' -import { Iot } from 'aws-sdk' +import { Certificate, ListCertificatesResponse } from '@aws-sdk/client-iot' export type CertGen = typeof getCertList @@ -53,8 +53,8 @@ export async function attachCertificateCommand(node: IotThingNode, promptFun = p /** * Prompts the user to pick a certificate to attach. */ -async function promptForCert(iot: IotClient, certFetch: CertGen): Promise> { - const placeHolder: DataQuickPickItem = { +async function promptForCert(iot: IotClient, certFetch: CertGen): Promise> { + const placeHolder: DataQuickPickItem = { label: 'No certificates found', data: undefined, } @@ -71,10 +71,10 @@ async function promptForCert(iot: IotClient, certFetch: CertGen): Promise> { - const placeHolder: DataQuickPickItem = { +async function promptForPolicy(iot: IotClient, policyFetch: PolicyGen): Promise> { + const placeHolder: DataQuickPickItem = { label: 'No policies found', data: undefined, } @@ -87,10 +87,10 @@ async function promptForPolicy(iot: IotClient, policyFetch: PolicyGen): Promise< */ async function* getPolicyList(iot: IotClient) { let marker: string | undefined = undefined - let filteredPolicies: Iot.Policy[] + let filteredPolicies: Policy[] do { try { - const policyResponse: Iot.ListPoliciesResponse = await iot.listPolicies({ marker }) + const policyResponse: ListPoliciesResponse = await iot.listPolicies({ marker }) marker = policyResponse.nextMarker /* The policy name and arn should always be defined when using the diff --git a/packages/core/src/awsService/iot/commands/createCert.ts b/packages/core/src/awsService/iot/commands/createCert.ts index c954de28886..7cd9d4b62ed 100644 --- a/packages/core/src/awsService/iot/commands/createCert.ts +++ b/packages/core/src/awsService/iot/commands/createCert.ts @@ -10,7 +10,7 @@ import { localize } from '../../../shared/utilities/vsCodeUtils' import { showViewLogsMessage } from '../../../shared/utilities/messages' import { IotCertsFolderNode } from '../explorer/iotCertFolderNode' import { fileExists } from '../../../shared/filesystemUtilities' -import { Iot } from 'aws-sdk' +import { CreateKeysAndCertificateResponse } from '@aws-sdk/client-iot' import { fs } from '../../../shared/fs/fs' // eslint-disable-next-line @typescript-eslint/naming-convention @@ -34,7 +34,7 @@ export async function createCertificateCommand( return } - let certificate: Iot.CreateKeysAndCertificateResponse + let certificate: CreateKeysAndCertificateResponse try { certificate = await node.iot.createCertificateAndKeys({ diff --git a/packages/core/src/awsService/iot/explorer/iotPolicyNode.ts b/packages/core/src/awsService/iot/explorer/iotPolicyNode.ts index 3f9b7003d60..ce104a5e7fa 100644 --- a/packages/core/src/awsService/iot/explorer/iotPolicyNode.ts +++ b/packages/core/src/awsService/iot/explorer/iotPolicyNode.ts @@ -4,7 +4,6 @@ */ import * as vscode from 'vscode' -import { Iot } from 'aws-sdk' import { IotClient, IotPolicy } from '../../../shared/clients/iotClient' import { AWSResourceNode } from '../../../shared/treeview/nodes/awsResourceNode' @@ -20,6 +19,7 @@ import { localize } from '../../../shared/utilities/vsCodeUtils' import { getIcon } from '../../../shared/icons' import { Settings } from '../../../shared/settings' import { ClassToInterfaceType } from '../../../shared/utilities/tsUtils' +import { PolicyVersion } from '@aws-sdk/client-iot' /** * Represents an IoT Policy that may have either a Certificate Node or the @@ -101,7 +101,7 @@ export class IotPolicyWithVersionsNode extends IotPolicyNode { } public async updateChildren(): Promise { - const versions: Map = toMap( + const versions: Map = toMap( await toArrayAsync(this.iot.listPolicyVersions({ policyName: this.policy.name })), (version) => version.versionId ) diff --git a/packages/core/src/awsService/iot/explorer/iotPolicyVersionNode.ts b/packages/core/src/awsService/iot/explorer/iotPolicyVersionNode.ts index ab2dd0f872d..bd85eb6e8c5 100644 --- a/packages/core/src/awsService/iot/explorer/iotPolicyVersionNode.ts +++ b/packages/core/src/awsService/iot/explorer/iotPolicyVersionNode.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Iot } from 'aws-sdk' +import { PolicyVersion } from '@aws-sdk/client-iot' import { IotClient, IotPolicy } from '../../../shared/clients/iotClient' import { AWSResourceNode } from '../../../shared/treeview/nodes/awsResourceNode' import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' @@ -19,7 +19,7 @@ import { formatLocalized } from '../../../shared/datetime' export class IotPolicyVersionNode extends AWSTreeNodeBase implements AWSResourceNode { public constructor( public policy: IotPolicy, - public version: Iot.PolicyVersion, + public version: PolicyVersion, public isDefault: boolean, public readonly parent: IotPolicyWithVersionsNode, public readonly iot: IotClient @@ -35,7 +35,7 @@ export class IotPolicyVersionNode extends AWSTreeNodeBase implements AWSResource this.update(version) } - public update(version: Iot.PolicyVersion): void { + public update(version: PolicyVersion): void { this.version = version this.isDefault = version.isDefaultVersion ?? false this.tooltip = localize( diff --git a/packages/core/src/awsService/redshift/explorer/redshiftWarehouseNode.ts b/packages/core/src/awsService/redshift/explorer/redshiftWarehouseNode.ts index 4301d45378b..26947893c82 100644 --- a/packages/core/src/awsService/redshift/explorer/redshiftWarehouseNode.ts +++ b/packages/core/src/awsService/redshift/explorer/redshiftWarehouseNode.ts @@ -15,7 +15,7 @@ import { ChildNodeLoader, ChildNodePage } from '../../../awsexplorer/childNodeLo import { DefaultRedshiftClient } from '../../../shared/clients/redshiftClient' import { deleteConnection, ConnectionParams, ConnectionType, RedshiftWarehouseType } from '../models/models' import { RedshiftNodeConnectionWizard } from '../wizards/connectionWizard' -import { ListDatabasesResponse } from 'aws-sdk/clients/redshiftdata' +import { ListDatabasesResponse } from '@aws-sdk/client-redshift-data' import { getIcon } from '../../../shared/icons' import { AWSCommandTreeNode } from '../../../shared/treeview/nodes/awsCommandTreeNode' import { telemetry } from '../../../shared/telemetry/telemetry' diff --git a/packages/core/src/awsService/redshift/notebook/redshiftNotebookController.ts b/packages/core/src/awsService/redshift/notebook/redshiftNotebookController.ts index 1db972314d3..f71b93362ee 100644 --- a/packages/core/src/awsService/redshift/notebook/redshiftNotebookController.ts +++ b/packages/core/src/awsService/redshift/notebook/redshiftNotebookController.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode' import { DefaultRedshiftClient } from '../../../shared/clients/redshiftClient' import { ConnectionParams } from '../models/models' -import { RedshiftData } from 'aws-sdk' +import { ColumnMetadata, Field } from '@aws-sdk/client-redshift-data' import { telemetry } from '../../../shared/telemetry/telemetry' export class RedshiftNotebookController { @@ -79,8 +79,8 @@ export class RedshiftNotebookController { } let executionId: string | undefined - let columnMetadata: RedshiftData.ColumnMetadataList | undefined - const records: RedshiftData.SqlRecords = [] + let columnMetadata: ColumnMetadata[] | undefined + const records: Field[][] = [] let nextToken: string | undefined // get all the pages of the result do { @@ -90,7 +90,7 @@ export class RedshiftNotebookController { nextToken, executionId ) - if (result) { + if (result && result.statementResultResponse.Records) { nextToken = result.statementResultResponse.NextToken executionId = result.executionId columnMetadata = result.statementResultResponse.ColumnMetadata @@ -116,7 +116,7 @@ export class RedshiftNotebookController { }) } - public getAsTable(connectionParams: ConnectionParams, columns: string[], records: RedshiftData.SqlRecords) { + public getAsTable(connectionParams: ConnectionParams, columns: string[], records: Field[][]) { if (!records || records.length === 0) { return '

No records to display

' } diff --git a/packages/core/src/awsService/redshift/wizards/connectionWizard.ts b/packages/core/src/awsService/redshift/wizards/connectionWizard.ts index abaf99003a0..251cfd22e20 100644 --- a/packages/core/src/awsService/redshift/wizards/connectionWizard.ts +++ b/packages/core/src/awsService/redshift/wizards/connectionWizard.ts @@ -14,9 +14,9 @@ import { DefaultRedshiftClient } from '../../../shared/clients/redshiftClient' import { Region } from '../../../shared/regions/endpoints' import { RegionProvider } from '../../../shared/regions/regionProvider' import { createRegionPrompter } from '../../../shared/ui/common/region' -import { ClustersMessage } from 'aws-sdk/clients/redshift' +import { ClustersMessage } from '@aws-sdk/client-redshift' import { Prompter } from '../../../shared/ui/prompter' -import { ListSecretsResponse } from 'aws-sdk/clients/secretsmanager' +import { ListSecretsResponse } from '@aws-sdk/client-secrets-manager' import { SecretsManagerClient } from '../../../shared/clients/secretsManagerClient' import { redshiftHelpUrl } from '../../../shared/constants' diff --git a/packages/core/src/awsService/sagemaker/activation.ts b/packages/core/src/awsService/sagemaker/activation.ts index da8392ebad4..1a537fbc0f9 100644 --- a/packages/core/src/awsService/sagemaker/activation.ts +++ b/packages/core/src/awsService/sagemaker/activation.ts @@ -7,13 +7,20 @@ import * as path from 'path' import * as vscode from 'vscode' import { Commands } from '../../shared/vscode/commands2' import { SagemakerSpaceNode } from './explorer/sagemakerSpaceNode' -import { SagemakerParentNode } from './explorer/sagemakerParentNode' +import { SagemakerStudioNode } from './explorer/sagemakerStudioNode' import * as uriHandlers from './uriHandlers' import { openRemoteConnect, filterSpaceAppsByDomainUserProfiles, stopSpace } from './commands' import { updateIdleFile, startMonitoringTerminalActivity, ActivityCheckInterval } from './utils' import { ExtContext } from '../../shared/extensions' import { telemetry } from '../../shared/telemetry/telemetry' import { isSageMaker, UserActivity } from '../../shared/extensionUtilities' +import { SagemakerDevSpaceNode } from './explorer/sagemakerDevSpaceNode' +import { + filterDevSpacesByNamespaceCluster, + openHyperPodRemoteConnection, + stopHyperPodSpaceCommand, +} from './hyperpodCommands' +import { SagemakerHyperpodNode } from './explorer/sagemakerHyperpodNode' let terminalActivityInterval: NodeJS.Timeout | undefined @@ -29,7 +36,7 @@ export async function activate(ctx: ExtContext): Promise { }) }), - Commands.register('aws.sagemaker.filterSpaceApps', async (node: SagemakerParentNode) => { + Commands.register('aws.sagemaker.filterSpaceApps', async (node: SagemakerStudioNode) => { await telemetry.sagemaker_filterSpaces.run(async () => { await filterSpaceAppsByDomainUserProfiles(node) }) @@ -42,6 +49,30 @@ export async function activate(ctx: ExtContext): Promise { await telemetry.sagemaker_stopSpace.run(async () => { await stopSpace(node, ctx.extensionContext) }) + }), + + Commands.register('aws.hyperpod.filterDevSpaces', async (node: SagemakerHyperpodNode) => { + await telemetry.hyperpod_filterSpaces.run(async () => { + await filterDevSpacesByNamespaceCluster(node) + }) + }), + + Commands.register('aws.hyperpod.stopSpace', async (node: SagemakerDevSpaceNode) => { + if (!validateNode(node)) { + return + } + await telemetry.hyperpod_stopSpace.run(async () => { + await stopHyperPodSpaceCommand(node) + }) + }), + + Commands.register('aws.hyperpod.openRemoteConnection', async (node: SagemakerDevSpaceNode) => { + await telemetry.hyperpod_openRemoteConnection.run(async () => { + if (!validateNode(node)) { + return + } + await openHyperPodRemoteConnection(node) + }) }) ) diff --git a/packages/core/src/awsService/sagemaker/commands.ts b/packages/core/src/awsService/sagemaker/commands.ts index 0075d7e5dff..c0160fa19ba 100644 --- a/packages/core/src/awsService/sagemaker/commands.ts +++ b/packages/core/src/awsService/sagemaker/commands.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode' import * as nls from 'vscode-nls' import { SagemakerConstants } from './explorer/constants' -import { SagemakerParentNode } from './explorer/sagemakerParentNode' +import { SagemakerStudioNode } from './explorer/sagemakerStudioNode' import { DomainKeyDelimiter } from './utils' import { startVscodeRemote } from '../../shared/extensions/ssh' import { getLogger } from '../../shared/logger/logger' @@ -16,19 +16,29 @@ import _ from 'lodash' import { prepareDevEnvConnection, tryRemoteConnection } from './model' import { ExtContext } from '../../shared/extensions' import { SagemakerClient } from '../../shared/clients/sagemaker' -import { ToolkitError } from '../../shared/errors' +import { AccessDeniedException } from '@amzn/sagemaker-client' +import { ToolkitError, isUserCancelledError } from '../../shared/errors' import { showConfirmationMessage } from '../../shared/utilities/messages' import { RemoteSessionError } from '../../shared/remoteSession' -import { ConnectFromRemoteWorkspaceMessage, InstanceTypeError } from './constants' +import { + ConnectFromRemoteWorkspaceMessage, + InstanceTypeInsufficientMemory, + InstanceTypeInsufficientMemoryMessage, + RemoteAccess, + RemoteAccessRequiredMessage, + SpaceStatus, +} from './constants' +import { SagemakerUnifiedStudioSpaceNode } from '../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode' +import { node } from 'webpack' const localize = nls.loadMessageBundle() -export async function filterSpaceAppsByDomainUserProfiles(parentNode: SagemakerParentNode): Promise { - if (parentNode.domainUserProfiles.size === 0) { - // if parentNode has not been expanded, domainUserProfiles will be empty +export async function filterSpaceAppsByDomainUserProfiles(studioNode: SagemakerStudioNode): Promise { + if (studioNode.domainUserProfiles.size === 0) { + // if studioNode has not been expanded, domainUserProfiles will be empty // if so, this will attempt to populate domainUserProfiles - await parentNode.updateChildren() - if (parentNode.domainUserProfiles.size === 0) { + await studioNode.updateChildren() + if (studioNode.domainUserProfiles.size === 0) { getLogger().info(SagemakerConstants.NoSpaceToFilter) void vscode.window.showInformationMessage(SagemakerConstants.NoSpaceToFilter) return @@ -37,7 +47,7 @@ export async function filterSpaceAppsByDomainUserProfiles(parentNode: SagemakerP // Sort by domain name and user profile const sortedDomainUserProfiles = new Map( - [...parentNode.domainUserProfiles].sort((a, b) => { + [...studioNode.domainUserProfiles].sort((a, b) => { const domainNameA = a[1].domain.DomainName || '' const domainNameB = b[1].domain.DomainName || '' @@ -48,7 +58,7 @@ export async function filterSpaceAppsByDomainUserProfiles(parentNode: SagemakerP }) ) - const previousSelection = await parentNode.getSelectedDomainUsers() + const previousSelection = await studioNode.getSelectedDomainUsers() const items: (vscode.QuickPickItem & { key: string })[] = [] for (const [key, userMetadata] of sortedDomainUserProfiles) { @@ -74,8 +84,8 @@ export async function filterSpaceAppsByDomainUserProfiles(parentNode: SagemakerP const newSelection = result.map((r) => r.key) if (newSelection.length !== previousSelection.size || newSelection.some((key) => !previousSelection.has(key))) { - parentNode.saveSelectedDomainUsers(newSelection) - await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', parentNode) + studioNode.saveSelectedDomainUsers(newSelection) + await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', studioNode) } } @@ -85,10 +95,33 @@ export async function deeplinkConnect( session: string, wsUrl: string, token: string, - domain: string + domain: string, + appType?: string, + workspaceName?: string, + namespace?: string, + eksClusterArn?: string, + isSMUS: boolean = false ) { getLogger().debug( - `sm:deeplinkConnect: connectionIdentifier: ${connectionIdentifier} session: ${session} wsUrl: ${wsUrl} token: ${token}` + 'sm:deeplinkConnect: connectionIdentifier: %s session: %s wsUrl: %s token: %s isSMUS: %s', + connectionIdentifier, + session, + wsUrl, + token, + isSMUS + ) + + getLogger().info( + `sm:deeplinkConnect: + domain: ${domain}, + appType: ${appType}, + workspaceName: ${workspaceName}, + namespace: ${namespace}, + eksClusterArn: ${eksClusterArn}` + ) + + getLogger().info( + `sm:deeplinkConnect: domain: ${domain}, appType: ${appType}, workspaceName: ${workspaceName}, namespace: ${namespace}, eksClusterArn: ${eksClusterArn}` ) if (isRemoteWorkspace()) { @@ -97,35 +130,69 @@ export async function deeplinkConnect( } try { + let connectionType = 'sm_dl' + if (domain === '') { + connectionType = 'sm_hp' + } const remoteEnv = await prepareDevEnvConnection( connectionIdentifier, ctx.extensionContext, - 'sm_dl', + connectionType, + isSMUS /* isSMUS */, + undefined /* node */, session, wsUrl, token, - domain + domain, + appType ) - await startVscodeRemote( - remoteEnv.SessionProcess, - remoteEnv.hostname, - '/home/sagemaker-user', - remoteEnv.vscPath, - 'sagemaker-user' - ) + try { + await startVscodeRemote( + remoteEnv.SessionProcess, + remoteEnv.hostname, + '/home/sagemaker-user', + remoteEnv.vscPath, + 'sagemaker-user' + ) + } catch (remoteErr: any) { + throw new ToolkitError( + `Failed to establish remote connection: ${remoteErr.message}. Check Remote-SSH logs for details.`, + { cause: remoteErr, code: remoteErr.code || 'RemoteConnectionFailed' } + ) + } } catch (err: any) { getLogger().error( - `sm:OpenRemoteConnect: Unable to connect to target space with arn: ${connectionIdentifier} error: ${err}` + 'sm:OpenRemoteConnect: Unable to connect to target space with arn: %s error: %s isSMUS: %s', + connectionIdentifier, + err, + isSMUS ) if (![RemoteSessionError.MissingExtension, RemoteSessionError.ExtensionVersionTooLow].includes(err.code)) { + void vscode.window.showErrorMessage( + `Remote connection failed: ${err.message || 'Unknown error'}. Check Output > Log (Window) for details.` + ) throw err } } } -export async function stopSpace(node: SagemakerSpaceNode, ctx: vscode.ExtensionContext) { +export async function stopSpace( + node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode, + ctx: vscode.ExtensionContext, + sageMakerClient?: SagemakerClient +) { + await tryRefreshNode(node) + if (node.getStatus() === SpaceStatus.STOPPED || node.getStatus() === SpaceStatus.STOPPING) { + void vscode.window.showWarningMessage(`Space ${node.spaceApp.SpaceName} is already in Stopped/Stopping state.`) + return + } else if (node.getStatus() === SpaceStatus.STARTING) { + void vscode.window.showWarningMessage( + `Space ${node.spaceApp.SpaceName} is in Starting state. Wait until it is Running to attempt stop again.` + ) + return + } const spaceName = node.spaceApp.SpaceName! const confirmed = await showConfirmationMessage({ prompt: `You are about to stop this space. Any active resource will also be stopped. Are you sure you want to stop the space?`, @@ -137,53 +204,268 @@ export async function stopSpace(node: SagemakerSpaceNode, ctx: vscode.ExtensionC if (!confirmed) { return } - - const client = new SagemakerClient(node.regionCode) + // In case of SMUS, we pass in a SM Client and for SM AI, it creates a new SM Client. + const client = sageMakerClient ? sageMakerClient : new SagemakerClient(node.regionCode) try { await client.deleteApp({ DomainId: node.spaceApp.DomainId!, SpaceName: spaceName, - AppType: node.spaceApp.App!.AppType!, + AppType: node.spaceApp.SpaceSettingsSummary!.AppType!, AppName: node.spaceApp.App?.AppName, }) } catch (err) { const error = err as Error - if (error.name === 'AccessDeniedException') { + if (error instanceof AccessDeniedException) { throw new ToolkitError('You do not have permission to stop spaces. Please contact your administrator', { cause: error, + code: error.name, }) } else { - throw err + throw new ToolkitError(`Failed to stop space ${spaceName}: ${(error as Error).message}`, { + cause: error, + code: error.name, + }) } } await tryRefreshNode(node) } -export async function openRemoteConnect(node: SagemakerSpaceNode, ctx: vscode.ExtensionContext) { +export async function openRemoteConnect( + node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode, + ctx: vscode.ExtensionContext, + sageMakerClient?: SagemakerClient +) { if (isRemoteWorkspace()) { void vscode.window.showErrorMessage(ConnectFromRemoteWorkspaceMessage) return } - if (node.getStatus() === 'Stopped') { - const client = new SagemakerClient(node.regionCode) + try { + const spaceName = node.spaceApp.SpaceName! + await tryRefreshNode(node) - try { - await client.startSpace(node.spaceApp.SpaceName!, node.spaceApp.DomainId!) - await tryRefreshNode(node) - const appType = node.spaceApp.SpaceSettingsSummary?.AppType - if (!appType) { - throw new ToolkitError('AppType is undefined for the selected space. Cannot start remote connection.') + const remoteAccess = node.spaceApp.SpaceSettingsSummary?.RemoteAccess + const nodeStatus = node.getStatus() + + // Route to appropriate handler based on space state + if (nodeStatus === SpaceStatus.RUNNING && remoteAccess !== RemoteAccess.ENABLED) { + return await handleRunningSpaceWithDisabledAccess(node, ctx, spaceName, sageMakerClient) + } else if (nodeStatus === SpaceStatus.STOPPED) { + return await handleStoppedSpace(node, ctx, spaceName, sageMakerClient) + } else if (nodeStatus === SpaceStatus.RUNNING) { + return await handleRunningSpaceWithEnabledAccess(node, ctx, spaceName) + } + } catch (err: any) { + // Suppress errors that don't need additional error messages: + // - User cancellations (checked by isUserCancelledError) + // - SSH config errors (already shown via modal in prepareDevEnvConnection) + if (isUserCancelledError(err) || (err instanceof ToolkitError && err.code === 'SshConfigError')) { + return + } + throw err + } +} + +/** + * Checks if an instance type upgrade will be needed for remote access + */ +export async function checkInstanceTypeUpgradeNeeded( + node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode, + sageMakerClient?: SagemakerClient +): Promise<{ upgradeNeeded: boolean; currentType?: string; recommendedType?: string }> { + const client = sageMakerClient || new SagemakerClient(node.regionCode) + + try { + const spaceDetails = await client.describeSpace({ + DomainId: node.spaceApp.DomainId!, + SpaceName: node.spaceApp.SpaceName!, + }) + + const appType = spaceDetails.SpaceSettings!.AppType! + + // Get current instance type + const currentResourceSpec = + appType === 'JupyterLab' + ? spaceDetails.SpaceSettings!.JupyterLabAppSettings?.DefaultResourceSpec + : spaceDetails.SpaceSettings!.CodeEditorAppSettings?.DefaultResourceSpec + + const currentInstanceType = currentResourceSpec?.InstanceType + + // Check if upgrade is needed + if (currentInstanceType && currentInstanceType in InstanceTypeInsufficientMemory) { + // Current type has insufficient memory + return { + upgradeNeeded: true, + currentType: currentInstanceType, + recommendedType: InstanceTypeInsufficientMemory[currentInstanceType], + } + } + + return { upgradeNeeded: false, currentType: currentInstanceType } + } catch (err) { + const error = err as Error + if (error instanceof AccessDeniedException) { + throw new ToolkitError('You do not have permission to describe spaces. Please contact your administrator', { + cause: error, + code: error.name, + }) + } + throw err + } +} + +/** + * Handles connecting to a running space with disabled remote access + * Requires stopping the space, enabling remote access, and restarting + */ +async function handleRunningSpaceWithDisabledAccess( + node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode, + ctx: vscode.ExtensionContext, + spaceName: string, + sageMakerClient?: SagemakerClient +) { + // Check if instance type upgrade will be needed + const instanceTypeInfo = await checkInstanceTypeUpgradeNeeded(node, sageMakerClient) + + let prompt: string + if (instanceTypeInfo.upgradeNeeded) { + prompt = InstanceTypeInsufficientMemoryMessage( + spaceName, + instanceTypeInfo.currentType!, + instanceTypeInfo.recommendedType! + ) + } else { + // Only remote access needs to be enabled + prompt = RemoteAccessRequiredMessage + } + + const confirmed = await showConfirmationMessage({ + prompt, + confirm: 'Restart Space and Connect', + cancel: 'Cancel', + type: 'warning', + }) + + if (!confirmed) { + return + } + + // Enable remote access and connect + const client = sageMakerClient || new SagemakerClient(node.regionCode) + + return await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: `Connecting to ${spaceName}`, + }, + async (progress) => { + try { + // Show initial progress message + progress.report({ message: 'Stopping the space' }) + + // Stop the running space + await client.deleteApp({ + DomainId: node.spaceApp.DomainId!, + SpaceName: spaceName, + AppType: node.spaceApp.SpaceSettingsSummary!.AppType!, + AppName: node.spaceApp.App?.AppName, + }) + + // Update progress message + progress.report({ message: 'Starting the space' }) + + // Start the space with remote access enabled (skip prompts since user already consented) + await client.startSpace(spaceName, node.spaceApp.DomainId!, true) + await tryRefreshNode(node) + await client.waitForAppInService( + node.spaceApp.DomainId!, + spaceName, + node.spaceApp.SpaceSettingsSummary!.AppType!, + progress + ) + await tryRemoteConnection(node, ctx, progress) + } catch (err: any) { + // Suppress errors that don't need additional error messages: + // - User cancellations (checked by isUserCancelledError) + // - SSH config errors (already shown via modal in prepareDevEnvConnection) + if (isUserCancelledError(err) || (err instanceof ToolkitError && err.code === 'SshConfigError')) { + return + } + throw new ToolkitError(`Remote connection failed: ${err.message}`, { + cause: err, + code: err.code, + }) } - await client.waitForAppInService(node.spaceApp.DomainId!, node.spaceApp.SpaceName!, appType) - await tryRemoteConnection(node, ctx) - } catch (err: any) { - // Ignore InstanceTypeError since it means the user decided not to use an instanceType with more memory - if (err.code !== InstanceTypeError) { - throw err + } + ) +} + +/** + * Handles connecting to a stopped space + * Starts the space and connects (remote access enabled automatically if needed) + */ +async function handleStoppedSpace( + node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode, + ctx: vscode.ExtensionContext, + spaceName: string, + sageMakerClient?: SagemakerClient +) { + const client = sageMakerClient || new SagemakerClient(node.regionCode) + + try { + await client.startSpace(spaceName, node.spaceApp.DomainId!) + await tryRefreshNode(node) + + return await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: `Connecting to ${spaceName}`, + }, + async (progress) => { + progress.report({ message: 'Starting the space' }) + await client.waitForAppInService( + node.spaceApp.DomainId!, + spaceName, + node.spaceApp.SpaceSettingsSummary!.AppType!, + progress + ) + await tryRemoteConnection(node, ctx, progress) } + ) + } catch (err: any) { + // Suppress errors that don't need additional error messages: + // - User cancellations (checked by isUserCancelledError) + // - SSH config errors (already shown via modal in prepareDevEnvConnection) + if (isUserCancelledError(err) || (err instanceof ToolkitError && err.code === 'SshConfigError')) { + return } - } else if (node.getStatus() === 'Running') { - await tryRemoteConnection(node, ctx) + throw new ToolkitError(`Remote connection failed: ${(err as Error).message}`, { + cause: err as Error, + code: err.code, + }) } } + +/** + * Handles connecting to a running space with enabled remote access + * Direct connection without any space modifications + */ +async function handleRunningSpaceWithEnabledAccess( + node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode, + ctx: vscode.ExtensionContext, + spaceName: string, + sageMakerClient?: SagemakerClient +) { + return await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: `Connecting to ${spaceName}`, + }, + async (progress) => { + await tryRemoteConnection(node, ctx, progress) + } + ) +} diff --git a/packages/core/src/awsService/sagemaker/constants.ts b/packages/core/src/awsService/sagemaker/constants.ts index 1fc51a1d20d..8e2ce84a0eb 100644 --- a/packages/core/src/awsService/sagemaker/constants.ts +++ b/packages/core/src/awsService/sagemaker/constants.ts @@ -7,6 +7,7 @@ export const ConnectFromRemoteWorkspaceMessage = 'Unable to establish new remote connection. Your last active VS Code window is connected to a remote workspace. To open a new SageMaker Studio connection, select your local VS Code window and try again.' export const InstanceTypeError = 'InstanceTypeError' +export const SshConfigError = 'SshConfigError' export const InstanceTypeMinimum = 'ml.t3.large' @@ -18,14 +19,42 @@ export const InstanceTypeInsufficientMemory: Record = { 'ml.c5.large': 'ml.c5.xlarge', } +// Remote access constants +export const RemoteAccess = { + ENABLED: 'ENABLED', + DISABLED: 'DISABLED', +} as const + +export const SpaceStatus = { + RUNNING: 'Running', + STOPPED: 'Stopped', + STARTING: 'Starting', + STOPPING: 'Stopping', +} as const + export const InstanceTypeInsufficientMemoryMessage = ( spaceName: string, chosenInstanceType: string, recommendedInstanceType: string ) => { - return `Unable to create app for [${spaceName}] because instanceType [${chosenInstanceType}] is not supported for remote access enabled spaces. Use instanceType with at least 8 GiB memory. Would you like to start your space with instanceType [${recommendedInstanceType}]?` + return `[${chosenInstanceType}] does not support remote access. Use an instanceType with at least 8 GiB memory. Would you like to start your space with instanceType [${recommendedInstanceType}]?` } export const InstanceTypeNotSelectedMessage = (spaceName: string) => { return `No instanceType specified for [${spaceName}]. ${InstanceTypeMinimum} is the default instance type, which meets minimum 8 GiB memory requirements for remote access. Continuing will start your space with instanceType [${InstanceTypeMinimum}] and remotely connect.` } + +export const RemoteAccessRequiredMessage = + 'This space requires remote access to be enabled.\nWould you like to restart the space and connect?\nAny unsaved work will be lost.' + +export const SshConfigErrorMessage = () => { + return `Unable to connect. Your SSH config file contains errors. Fix the errors to continue.` +} + +export const SmusDeeplinkSessionExpiredError = { + title: 'Session Disconnected', + message: + 'Your SageMaker Unified Studio session has been disconnected. Select a local (non-remote) VS Code window and use the SageMaker Unified Studio portal to connect again.', + code: 'SMUS_SESSION_DISCONNECTED', + shortMessage: 'Session disconnected, re-connect from SageMaker Unified Studio portal.', +} as const diff --git a/packages/core/src/awsService/sagemaker/credentialMapping.ts b/packages/core/src/awsService/sagemaker/credentialMapping.ts index 60d4e94260e..931384e8811 100644 --- a/packages/core/src/awsService/sagemaker/credentialMapping.ts +++ b/packages/core/src/awsService/sagemaker/credentialMapping.ts @@ -13,6 +13,9 @@ import { Auth } from '../../auth/auth' import { SpaceMappings, SsmConnectionInfo } from './types' import { getLogger } from '../../shared/logger/logger' import { parseArn } from './detached-server/utils' +import { SagemakerUnifiedStudioSpaceNode } from '../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode' +import { SageMakerUnifiedStudioSpacesParentNode } from '../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode' +import { isSmusSsoConnection } from '../../sagemakerunifiedstudio/auth/model' const mappingFileName = '.sagemaker-space-profiles' const mappingFilePath = path.join(os.homedir(), '.aws', mappingFileName) @@ -44,9 +47,9 @@ export async function saveMappings(data: SpaceMappings): Promise { /** * Persists the current profile to the appropriate space mapping based on connection type and profile format. - * @param appArn - The identifier for the SageMaker space. + * @param spaceArn - The arn for the SageMaker space. */ -export async function persistLocalCredentials(appArn: string): Promise { +export async function persistLocalCredentials(spaceArn: string): Promise { const currentProfileId = Auth.instance.getCurrentProfileId() if (!currentProfileId) { throw new ToolkitError('No current profile ID available for saving space credentials.') @@ -55,59 +58,86 @@ export async function persistLocalCredentials(appArn: string): Promise { if (currentProfileId.startsWith('sso:')) { const credentials = globals.loginManager.store.credentialsCache[currentProfileId] await setSpaceSsoProfile( - appArn, + spaceArn, credentials.credentials.accessKeyId, credentials.credentials.secretAccessKey, credentials.credentials.sessionToken ?? '' ) } else { - await setSpaceIamProfile(appArn, currentProfileId) + await setSpaceIamProfile(spaceArn, currentProfileId) } } +/** + * Persists the current selected SMUS Project Role creds to the appropriate space mapping. + * @param spaceArn - The identifier for the SageMaker Space. + */ +export async function persistSmusProjectCreds(spaceArn: string, node: SagemakerUnifiedStudioSpaceNode): Promise { + const nodeParent = node.getParent() as SageMakerUnifiedStudioSpacesParentNode + const authProvider = nodeParent.getAuthProvider() + const activeConnection = authProvider.activeConnection + const projectId = nodeParent.getProjectId() + const projectAuthProvider = await authProvider.getProjectCredentialProvider(projectId) + await projectAuthProvider.getCredentials() + await setSmusSpaceProfile(spaceArn, projectId, isSmusSsoConnection(activeConnection) ? 'sso' : 'iam') + // Trigger SSH credential refresh for the project + projectAuthProvider.startProactiveCredentialRefresh() +} + /** * Persists deep link credentials for a SageMaker space using a derived refresh URL based on environment. * - * @param appArn - ARN of the SageMaker space. + * @param spaceArn - ARN of the SageMaker space. * @param domain - The domain ID associated with the space. * @param session - SSM session ID. * @param wsUrl - SSM WebSocket URL. * @param token - Bearer token for the session. + * @param appType - Application type (e.g., 'jupyterlab', 'codeeditor'). + * @param isSMUS - If true, skip refreshUrl construction (SMUS connections cannot refresh). */ export async function persistSSMConnection( - appArn: string, + spaceArn: string, domain: string, session?: string, wsUrl?: string, - token?: string + token?: string, + appType?: string, + isSMUS?: boolean ): Promise { - const { region } = parseArn(appArn) - const endpoint = DevSettings.instance.get('endpoints', {})['sagemaker'] ?? '' + let refreshUrl: string | undefined + + if (!isSMUS) { + // Construct refreshUrl for SageMaker AI connections + const { region } = parseArn(spaceArn) + const endpoint = DevSettings.instance.get('endpoints', {})['sagemaker'] ?? '' - // TODO: Hardcoded to 'jupyterlab' due to a bug in Studio that only supports refreshing - // the token for both CodeEditor and JupyterLab Apps in the jupyterlab subdomain. - // This will be fixed shortly after NYSummit launch to support refresh URL in CodeEditor subdomain. - const appSubDomain = 'jupyterlab' + let appSubDomain = 'jupyterlab' + if (appType && appType.toLowerCase() === 'codeeditor') { + appSubDomain = 'code-editor' + } - let envSubdomain: string + let envSubdomain: string - if (endpoint.includes('beta')) { - envSubdomain = 'devo' - } else if (endpoint.includes('gamma')) { - envSubdomain = 'loadtest' - } else { - envSubdomain = 'studio' - } + if (endpoint.includes('beta')) { + envSubdomain = 'devo' + } else if (endpoint.includes('gamma')) { + envSubdomain = 'loadtest' + } else { + envSubdomain = 'studio' + } - // Use the standard AWS domain for 'studio' (prod). - // For non-prod environments, use the obfuscated domain 'asfiovnxocqpcry.com'. - const baseDomain = - envSubdomain === 'studio' - ? `studio.${region}.sagemaker.aws` - : `${envSubdomain}.studio.${region}.asfiovnxocqpcry.com` + // Use the standard AWS domain for 'studio' (prod). + // For non-prod environments, use the obfuscated domain 'asfiovnxocqpcry.com'. + const baseDomain = + envSubdomain === 'studio' + ? `studio.${region}.sagemaker.aws` + : `${envSubdomain}.studio.${region}.asfiovnxocqpcry.com` - const refreshUrl = `https://studio-${domain}.${baseDomain}/${appSubDomain}` - await setSpaceCredentials(appArn, refreshUrl, { + refreshUrl = `https://studio-${domain}.${baseDomain}/${appSubDomain}` + } + // For SMUS connections, refreshUrl remains undefined + + await setSpaceCredentials(spaceArn, refreshUrl, { sessionId: session ?? '-', url: wsUrl ?? '-', token: token ?? '-', @@ -116,51 +146,68 @@ export async function persistSSMConnection( /** * Sets or updates an IAM credential profile for a given space. - * @param spaceName - The name of the SageMaker space. + * @param spaceArn - The name of the SageMaker space. * @param profileName - The local AWS profile name to associate. */ -export async function setSpaceIamProfile(spaceName: string, profileName: string): Promise { +export async function setSpaceIamProfile(spaceArn: string, profileName: string): Promise { const data = await loadMappings() data.localCredential ??= {} - data.localCredential[spaceName] = { type: 'iam', profileName } + data.localCredential[spaceArn] = { type: 'iam', profileName } await saveMappings(data) } /** * Sets or updates an SSO credential profile for a given space. - * @param spaceName - The name of the SageMaker space. + * @param spaceArn - The arn of the SageMaker space. * @param accessKey - Temporary access key from SSO. * @param secret - Temporary secret key from SSO. * @param token - Session token from SSO. */ export async function setSpaceSsoProfile( - spaceName: string, + spaceArn: string, accessKey: string, secret: string, token: string ): Promise { const data = await loadMappings() data.localCredential ??= {} - data.localCredential[spaceName] = { type: 'sso', accessKey, secret, token } + data.localCredential[spaceArn] = { type: 'sso', accessKey, secret, token } + await saveMappings(data) +} + +/** + * Sets the SM Space to map to SageMaker Unified Studio Project. + * @param spaceArn - The arn of the SageMaker Unified Studio space. + * @param projectId - The project ID associated with the SageMaker Unified Studio space. + * @param credentialType - The type of credential ('sso' or 'iam'). + */ +export async function setSmusSpaceProfile( + spaceArn: string, + projectId: string, + credentialType: 'iam' | 'sso' +): Promise { + const data = await loadMappings() + data.localCredential ??= {} + data.localCredential[spaceArn] = { type: credentialType, smusProjectId: projectId } await saveMappings(data) } /** * Stores SSM connection information for a given space, typically from a deep link session. * This initializes the request as 'fresh' and includes a refresh URL if provided. - * @param spaceName - The name of the SageMaker space. - * @param refreshUrl - URL to use for refreshing session tokens. + * @param spaceArn - The arn of the SageMaker space. + * @param refreshUrl - URL to use for refreshing session tokens (undefined for SMUS connections). * @param credentials - The session information used to initiate the connection. */ export async function setSpaceCredentials( - spaceName: string, - refreshUrl: string, + spaceArn: string, + refreshUrl: string | undefined, credentials: SsmConnectionInfo ): Promise { const data = await loadMappings() data.deepLink ??= {} - data.deepLink[spaceName] = { + data.deepLink[spaceArn] = { refreshUrl, requests: { 'initial-connection': { diff --git a/packages/core/src/awsService/sagemaker/detached-server/credentials.ts b/packages/core/src/awsService/sagemaker/detached-server/credentials.ts index 5b2a7fdbc64..033cf1d3b8e 100644 --- a/packages/core/src/awsService/sagemaker/detached-server/credentials.ts +++ b/packages/core/src/awsService/sagemaker/detached-server/credentials.ts @@ -29,22 +29,51 @@ export async function resolveCredentialsFor(connectionIdentifier: string): Promi switch (profile.type) { case 'iam': { - const name = profile.profileName?.split(':')[1] - if (!name) { - throw new Error(`Invalid IAM profile name for "${connectionIdentifier}"`) + if ('profileName' in profile) { + const name = profile.profileName?.split(':')[1] + if (!name) { + throw new Error(`Invalid IAM profile name for "${connectionIdentifier}"`) + } + return fromIni({ profile: name }) + } else if ('smusProjectId' in profile) { + const { accessKey, secret, token } = mapping.smusProjects?.[profile.smusProjectId] || {} + if (!accessKey || !secret || !token) { + throw new Error(`Missing ProjectRole credentials for SMUS Space "${connectionIdentifier}"`) + } + return { + accessKeyId: accessKey, + secretAccessKey: secret, + sessionToken: token, + } + } else { + throw new Error(`Missing IAM credentials for "${connectionIdentifier}"`) } - return fromIni({ profile: name }) } case 'sso': { - const { accessKey, secret, token } = profile - if (!accessKey || !secret || !token) { + if ('accessKey' in profile && 'secret' in profile && 'token' in profile) { + const { accessKey, secret, token } = profile + if (!accessKey || !secret || !token) { + throw new Error(`Missing SSO credentials for "${connectionIdentifier}"`) + } + return { + accessKeyId: accessKey, + secretAccessKey: secret, + sessionToken: token, + } + } else if ('smusProjectId' in profile) { + // Handle SMUS project ID case + const { accessKey, secret, token } = mapping.smusProjects?.[profile.smusProjectId] || {} + if (!accessKey || !secret || !token) { + throw new Error(`Missing ProjectRole credentials for SMUS Space "${connectionIdentifier}"`) + } + return { + accessKeyId: accessKey, + secretAccessKey: secret, + sessionToken: token, + } + } else { throw new Error(`Missing SSO credentials for "${connectionIdentifier}"`) } - return { - accessKeyId: accessKey, - secretAccessKey: secret, - sessionToken: token, - } } default: throw new Error(`Unsupported profile type "${profile}"`) diff --git a/packages/core/src/awsService/sagemaker/detached-server/errorPage.ts b/packages/core/src/awsService/sagemaker/detached-server/errorPage.ts index e7c02c3e2f2..d1c88ca3fec 100644 --- a/packages/core/src/awsService/sagemaker/detached-server/errorPage.ts +++ b/packages/core/src/awsService/sagemaker/detached-server/errorPage.ts @@ -15,6 +15,7 @@ import { open } from './utils' export enum ExceptionType { ACCESS_DENIED = 'AccessDeniedException', DEFAULT = 'Default', + EXPIRED_TOKEN = 'ExpiredTokenException', INTERNAL_FAILURE = 'InternalFailure', RESOURCE_LIMIT_EXCEEDED = 'ResourceLimitExceeded', THROTTLING = 'ThrottlingException', @@ -31,13 +32,25 @@ export const getVSCodeErrorTitle = (error: SageMakerServiceException): string => return ErrorText.StartSession[ExceptionType.DEFAULT].Title } -export const getVSCodeErrorText = (error: SageMakerServiceException): string => { +export const getVSCodeErrorText = ( + error: SageMakerServiceException, + isSmus?: boolean, + isSmusIamConn?: boolean +): string => { const exceptionType = error.name as ExceptionType switch (exceptionType) { case ExceptionType.ACCESS_DENIED: case ExceptionType.VALIDATION: return ErrorText.StartSession[exceptionType].Text.replace('{message}', error.message) + case ExceptionType.EXPIRED_TOKEN: + // Use SMUS-specific message if in SMUS context + if (isSmus) { + return isSmusIamConn + ? ErrorText.StartSession[ExceptionType.EXPIRED_TOKEN].SmusIamText + : ErrorText.StartSession[ExceptionType.EXPIRED_TOKEN].SmusSsoText + } + return ErrorText.StartSession[exceptionType].Text case ExceptionType.INTERNAL_FAILURE: case ExceptionType.RESOURCE_LIMIT_EXCEEDED: case ExceptionType.THROTTLING: @@ -57,6 +70,14 @@ export const ErrorText = { Title: 'Unexpected system error', Text: 'We encountered an unexpected error: [{exceptionType}]. Please contact your administrator and provide them with this error so they can investigate the issue.', }, + [ExceptionType.EXPIRED_TOKEN]: { + Title: 'Authentication expired', + Text: 'Your session has expired. Please refresh your credentials and try again.', + SmusSsoText: + 'Your session has expired. This is likely due to network connectivity issues after machine sleep/resume. Wait 10-30 seconds for automatic credential refresh, then try again. If the issue persists, try reconnecting through AWS Toolkit.', + SmusIamText: + 'Your session has expired. Update the credentials associated with the IAM profile or use a valid IAM profile, then try again.', + }, [ExceptionType.INTERNAL_FAILURE]: { Title: 'Failed to connect remotely to VSCode', Text: 'Unable to establish remote connection to VSCode. This could be due to several factors. Please try again by clicking the VSCode button. If the problem persists, please contact your admin.', diff --git a/packages/core/src/awsService/sagemaker/detached-server/routes/getSession.ts b/packages/core/src/awsService/sagemaker/detached-server/routes/getSession.ts index a39b4c1c812..2db2d11ddeb 100644 --- a/packages/core/src/awsService/sagemaker/detached-server/routes/getSession.ts +++ b/packages/core/src/awsService/sagemaker/detached-server/routes/getSession.ts @@ -6,7 +6,7 @@ // Disabled: detached server files cannot import vscode. /* eslint-disable aws-toolkits/no-console-log */ import { IncomingMessage, ServerResponse } from 'http' -import { startSagemakerSession, parseArn } from '../utils' +import { startSagemakerSession, parseArn, isSmusConnection, isSmusIamConnection } from '../utils' import { resolveCredentialsFor } from '../credentials' import url from 'url' import { SageMakerServiceException } from '@amzn/sagemaker-client' @@ -33,6 +33,9 @@ export async function handleGetSession(req: IncomingMessage, res: ServerResponse } const { region } = parseArn(connectionIdentifier) + // Detect if this is a SMUS connection for specialized error handling + const isSmus = await isSmusConnection(connectionIdentifier) + const isSmusIamConn = await isSmusIamConnection(connectionIdentifier) try { const session = await startSagemakerSession({ region, connectionIdentifier, credentials }) @@ -48,7 +51,7 @@ export async function handleGetSession(req: IncomingMessage, res: ServerResponse const error = err as SageMakerServiceException console.error(`Failed to start SageMaker session for ${connectionIdentifier}:`, err) const errorTitle = getVSCodeErrorTitle(error) - const errorText = getVSCodeErrorText(error) + const errorText = getVSCodeErrorText(error, isSmus, isSmusIamConn) await openErrorPage(errorTitle, errorText) res.writeHead(500, { 'Content-Type': 'text/plain' }) res.end('Failed to start SageMaker session') diff --git a/packages/core/src/awsService/sagemaker/detached-server/routes/getSessionAsync.ts b/packages/core/src/awsService/sagemaker/detached-server/routes/getSessionAsync.ts index f8dad504067..c0db2712d07 100644 --- a/packages/core/src/awsService/sagemaker/detached-server/routes/getSessionAsync.ts +++ b/packages/core/src/awsService/sagemaker/detached-server/routes/getSessionAsync.ts @@ -9,6 +9,8 @@ import { IncomingMessage, ServerResponse } from 'http' import url from 'url' import { SessionStore } from '../sessionStore' import { open, parseArn, readServerInfo } from '../utils' +import { openErrorPage } from '../errorPage' +import { SmusDeeplinkSessionExpiredError } from '../../constants' export async function handleGetSessionAsync(req: IncomingMessage, res: ServerResponse): Promise { const parsedUrl = url.parse(req.url || '', true) @@ -46,8 +48,34 @@ export async function handleGetSessionAsync(req: IncomingMessage, res: ServerRes res.end() return } else if (status === 'not-started') { - const serverInfo = await readServerInfo() const refreshUrl = await store.getRefreshUrl(connectionIdentifier) + + // Check if this is a SMUS connection (no refreshUrl available) + if (refreshUrl === undefined) { + console.log(`SMUS session expired for connection: ${connectionIdentifier}`) + + // Clean up the expired connection entry + try { + await store.cleanupExpiredConnection(connectionIdentifier) + console.log(`Cleaned up expired connection: ${connectionIdentifier}`) + } catch (cleanupErr) { + console.error(`Failed to cleanup expired connection: ${cleanupErr}`) + // Continue with error response even if cleanup fails + } + + await openErrorPage(SmusDeeplinkSessionExpiredError.title, SmusDeeplinkSessionExpiredError.message) + res.writeHead(400, { 'Content-Type': 'application/json' }) + res.end( + JSON.stringify({ + error: SmusDeeplinkSessionExpiredError.code, + message: SmusDeeplinkSessionExpiredError.shortMessage, + }) + ) + return + } + + // Continue with existing SageMaker AI refresh flow + const serverInfo = await readServerInfo() const { spaceName } = parseArn(connectionIdentifier) const url = `${refreshUrl}/${encodeURIComponent(spaceName)}?remote_access_token_refresh=true&reconnect_identifier=${encodeURIComponent( diff --git a/packages/core/src/awsService/sagemaker/detached-server/sessionStore.ts b/packages/core/src/awsService/sagemaker/detached-server/sessionStore.ts index 04098f68c89..9a09ad2418d 100644 --- a/packages/core/src/awsService/sagemaker/detached-server/sessionStore.ts +++ b/packages/core/src/awsService/sagemaker/detached-server/sessionStore.ts @@ -9,7 +9,7 @@ import { readMapping, writeMapping } from './utils' export type SessionStatus = 'pending' | 'fresh' | 'consumed' | 'not-started' export class SessionStore { - async getRefreshUrl(connectionId: string) { + async getRefreshUrl(connectionId: string): Promise { const mapping = await readMapping() if (!mapping.deepLink) { @@ -21,10 +21,6 @@ export class SessionStore { throw new Error(`No mapping found for connectionId: "${connectionId}"`) } - if (!entry.refreshUrl) { - throw new Error(`No refreshUrl found for connectionId: "${connectionId}"`) - } - return entry.refreshUrl } @@ -113,6 +109,20 @@ export class SessionStore { await writeMapping(mapping) } + async cleanupExpiredConnection(connectionId: string) { + const mapping = await readMapping() + + if (!mapping.deepLink) { + throw new Error('No deepLink mapping found') + } + + // Remove the entire connection entry for the expired space + if (mapping.deepLink[connectionId]) { + delete mapping.deepLink[connectionId] + await writeMapping(mapping) + } + } + async setSession(connectionId: string, requestId: string, ssmConnectionInfo: SsmConnectionInfo) { const mapping = await readMapping() diff --git a/packages/core/src/awsService/sagemaker/detached-server/utils.ts b/packages/core/src/awsService/sagemaker/detached-server/utils.ts index de01041d4ad..fdbd1da1ab2 100644 --- a/packages/core/src/awsService/sagemaker/detached-server/utils.ts +++ b/packages/core/src/awsService/sagemaker/detached-server/utils.ts @@ -13,6 +13,7 @@ import os from 'os' import { join } from 'path' import { SpaceMappings } from '../types' import open from 'open' +import { ConfiguredRetryStrategy } from '@smithy/util-retry' export { open } export const mappingFilePath = join(os.homedir(), '.aws', '.sagemaker-space-profiles') @@ -22,6 +23,13 @@ const tempFilePath = `${mappingFilePath}.tmp` let isWriting = false const writeQueue: Array<() => Promise> = [] +// Currently SSM registration happens asynchronously with App launch, which can lead to +// StartSession Internal Failure when connecting to a fresly-started Space. +// To mitigate, spread out retries over multiple seconds instead of sending all retries within a second. +// Backoff sequence: 1500ms, 2250ms, 3375ms +// Retry timing: 1500ms, 3750ms, 7125ms +const startSessionRetryStrategy = new ConfiguredRetryStrategy(3, (attempt: number) => 1000 * 1.5 ** attempt) + /** * Reads the local endpoint info file (default or via env) and returns pid & port. * @throws Error if the file is missing, invalid JSON, or missing fields @@ -83,7 +91,7 @@ export function parseArn(arn: string): { region: string; accountId: string; spac export async function startSagemakerSession({ region, connectionIdentifier, credentials }: any) { const endpoint = process.env.SAGEMAKER_ENDPOINT || `https://sagemaker.${region}.amazonaws.com` - const client = new SageMakerClient({ region, credentials, endpoint }) + const client = new SageMakerClient({ region, credentials, endpoint, retryStrategy: startSessionRetryStrategy }) const command = new StartSessionCommand({ ResourceIdentifier: connectionIdentifier }) return client.send(command) } @@ -96,7 +104,6 @@ export async function readMapping() { try { const content = await fs.readFile(mappingFilePath, 'utf-8') console.log(`Mapping file path: ${mappingFilePath}`) - console.log(`Conents: ${content}`) return JSON.parse(content) } catch (err) { throw new Error(`Failed to read mapping file: ${err instanceof Error ? err.message : String(err)}`) @@ -122,6 +129,42 @@ async function processWriteQueue() { } } +/** + * Detects if the connection identifier is using SMUS credentials + * @param connectionIdentifier - The connection identifier to check + * @returns Promise - true if SMUS, false otherwise + */ +export async function isSmusConnection(connectionIdentifier: string): Promise { + try { + const mapping = await readMapping() + const profile = mapping.localCredential?.[connectionIdentifier] + + // Check if profile exists and has smusProjectId + return profile && 'smusProjectId' in profile + } catch (err) { + // If we can't read the mapping, assume not SMUS to avoid breaking existing functionality + return false + } +} + +/** + * Detects if the connection identifier is using SMUS IAM credentials + * @param connectionIdentifier - The connection identifier to check + * @returns Promise - true if SMUS IAM connection, false otherwise + */ +export async function isSmusIamConnection(connectionIdentifier: string): Promise { + try { + const mapping = await readMapping() + const profile = mapping.localCredential?.[connectionIdentifier] + + // Check if profile exists, has smusProjectId, and type is 'iam' + return profile && 'smusProjectId' in profile && profile.type === 'iam' + } catch (err) { + // If we can't detect it is iam connection, assume not SMUS IAM to avoid breaking existing functionality + return false + } +} + /** * Writes the mapping to a temp file and atomically renames it to the target path. * Uses a queue to prevent race conditions when multiple requests try to write simultaneously. diff --git a/packages/core/src/awsService/sagemaker/explorer/constants.ts b/packages/core/src/awsService/sagemaker/explorer/constants.ts index b9d7c3348b5..eb2464086f7 100644 --- a/packages/core/src/awsService/sagemaker/explorer/constants.ts +++ b/packages/core/src/awsService/sagemaker/explorer/constants.ts @@ -4,6 +4,11 @@ */ export abstract class SagemakerConstants { + static readonly HyperPodPlaceHolderMessage = '[No HyperPod Spaces Found]' + static readonly NoDevSpaceToFilter = 'No dev spaces to filter' + static readonly SelectedClusterNamespacesState = 'aws.hyperpod.selectedClusterNamespaces' + static readonly FilterHyperpodPlaceholderKey = 'aws.filterHyperpodSpacesPlaceholder' + static readonly FilterHyperpodPlaceholderMessage = 'Filter dev spaces by name spaces or cluster (unselect to hide)' static readonly PlaceHolderMessage = '[No Sagemaker Spaces Found]' static readonly EnableIdentityFilteringSetting = 'aws.sagemaker.studio.spaces.enableIdentityFiltering' static readonly SelectedDomainUsersState = 'aws.sagemaker.selectedDomainUsers' diff --git a/packages/core/src/awsService/sagemaker/explorer/sagemakerDevSpaceNode.ts b/packages/core/src/awsService/sagemaker/explorer/sagemakerDevSpaceNode.ts new file mode 100644 index 00000000000..92cd449c216 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/explorer/sagemakerDevSpaceNode.ts @@ -0,0 +1,140 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' +import { HyperpodCluster, HyperpodDevSpace } from '../../../shared/clients/kubectlClient' +import { SagemakerHyperpodNode } from './sagemakerHyperpodNode' +import { getIcon } from '../../../shared/icons' +import { getLogger } from '../../../shared/logger/logger' + +export const devSpaceContextValueStopped = 'awsSagemakerHyperpodDevSpaceStoppedNode' +export const devSpaceContextValueRunning = 'awsSagemakerHyperpodDevSpaceRunningNode' +export const devSpaceContextValueTransitional = 'awsSagemakerHyperpodDevSpaceTransitionalNode' +export const devSpaceContextValueError = 'awsSagemakerHyperpodDevSpaceErrorNode' + +export class SagemakerDevSpaceNode extends AWSTreeNodeBase { + public constructor( + public readonly parent: SagemakerHyperpodNode, + public readonly devSpace: HyperpodDevSpace, + public readonly hpCluster: HyperpodCluster, + public override readonly regionCode: string + ) { + super('') + this.updateWorkspace() + } + + public updateWorkspace() { + this.label = this.buildLabel() + this.description = this.buildDescription() + this.tooltip = new vscode.MarkdownString(this.buildTooltip()) + this.iconPath = this.buildIconPath() + this.contextValue = this.getContext() + if (this.isPending()) { + this.parent.trackPendingNode(this.getDevSpaceKey()) + } + } + + public buildLabel(): string { + return `${this.devSpace.name} (${this.devSpace.status})` + } + + public buildDescription(): string { + return `${this.devSpace.accessType ?? 'Public'} space` + } + + public buildTooltip(): string { + return `**Space:** ${this.devSpace.name} + \n**Namespace:** ${this.devSpace.namespace} + \n**Cluster:** ${this.devSpace.cluster} + \n**Creator:** ${this.devSpace.creator} + \n**Environment:** Hyperpod` + } + + public buildIconPath() { + switch (this.devSpace.appType) { + case 'jupyterlab': { + return getIcon('aws-sagemaker-jupyter-lab') + } + case 'code-editor': { + return getIcon('aws-sagemaker-code-editor') + } + default: { + break + } + } + } + + public getContext(): string { + if (this.status === 'Stopping' || this.status === 'Starting') { + return devSpaceContextValueTransitional + } else if (this.status === 'Stopped') { + return devSpaceContextValueStopped + } else if (this.status === 'Running') { + return devSpaceContextValueRunning + } else { + return devSpaceContextValueError + } + } + + public isPending(): boolean { + return ( + this.status !== 'Running' && + this.status !== 'Stopped' && + this.status !== 'Error' && + this.status !== 'Invalid' + ) + } + + public get status(): string { + return this.devSpace.status + } + + public get name(): string { + return this.devSpace.name + } + + public get namespace(): string { + return this.devSpace.namespace + } + + public get cluster(): string { + return this.devSpace.cluster + } + + public getParent(): SagemakerHyperpodNode { + return this.parent + } + + public getDevSpaceKey(): string { + return `${this.cluster}-${this.namespace}-${this.name}` + } + + public async updateWorkspaceStatus() { + try { + const kubectlClient = this.getParent().getKubectlClient(this.hpCluster.clusterName) + if (!kubectlClient) { + getLogger().info(`Failed to update workspace status due to unavailable kubectl client`) + return + } + this.devSpace.status = await kubectlClient.getHyperpodSpaceStatus(this.devSpace) + } catch (error) { + getLogger().warn( + '[Hyperpod] Failed to update status for %s: %s', + this.devSpace.name, + (error as Error).message + ) + } + this.updateWorkspace() + if (this.isPending()) { + this.parent.trackPendingNode(this.getDevSpaceKey()) + } + } + + public async refreshNode() { + await this.updateWorkspaceStatus() + await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', this) + } +} diff --git a/packages/core/src/awsService/sagemaker/explorer/sagemakerHyperpodNode.ts b/packages/core/src/awsService/sagemaker/explorer/sagemakerHyperpodNode.ts new file mode 100644 index 00000000000..f2d6f9f8ff9 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/explorer/sagemakerHyperpodNode.ts @@ -0,0 +1,233 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' +import { PlaceholderNode } from '../../../shared/treeview/nodes/placeholderNode' +import { makeChildrenNodes } from '../../../shared/treeview/utils' +import { HyperpodCluster, HyperpodDevSpace, KubectlClient } from '../../../shared/clients/kubectlClient' +import { SagemakerDevSpaceNode } from './sagemakerDevSpaceNode' +import { PollingSet } from '../../../shared/utilities/pollingSet' +import { SagemakerConstants } from './constants' +import { SagemakerClient } from '../../../shared/clients/sagemaker' +import { DescribeClusterCommand, EKSClient } from '@aws-sdk/client-eks' +import { GetCallerIdentityResponse } from 'aws-sdk/clients/sts' +import { DefaultStsClient } from '../../../shared/clients/stsClient' +import { updateInPlace } from '../../../shared/utilities/collectionUtils' +import { getLogger } from '../../../shared/logger/logger' +import globals from '../../../shared/extensionGlobals' + +export const hyperpodContextValue = 'awsSagemakerHyperpodNode' +export type SelectedClusterNamespaces = [string, string[]][] +export type SelectedClusterNameSpacesByRegion = [string, SelectedClusterNamespaces][] + +export class SagemakerHyperpodNode extends AWSTreeNodeBase { + public readonly hyperpodDevSpaceNodes: Map + public allSpaces: Map = new Map() + public readonly kubectlClients: Map = new Map() + public readonly eksClient: EKSClient + protected stsClient: DefaultStsClient + callerIdentity: GetCallerIdentityResponse = {} + clusterNamespaces: Map = new Map() + public readonly pollingSet: PollingSet = new PollingSet(5000, this.updatePendingNodes.bind(this)) + + public constructor( + public override readonly regionCode: string, + protected readonly sagemakerClient: SagemakerClient + ) { + super('HyperPod', vscode.TreeItemCollapsibleState.Collapsed) + this.contextValue = hyperpodContextValue + this.eksClient = this.sagemakerClient.getEKSClient() + this.stsClient = new DefaultStsClient(regionCode) + this.hyperpodDevSpaceNodes = new Map() + } + + public override async getChildren(): Promise { + const result = await makeChildrenNodes({ + getChildNodes: async () => { + await this.updateChildren() + return [...this.hyperpodDevSpaceNodes.values()] + }, + getNoChildrenPlaceholderNode: async () => + new PlaceholderNode(this, SagemakerConstants.HyperPodPlaceHolderMessage), + sort: (nodeA, nodeB) => nodeA.name.localeCompare(nodeB.name), + }) + + return result + } + + public async listSpaces(): Promise> { + try { + const clusters = await this.sagemakerClient.listHyperpodClusters() + if (!clusters) { + void vscode.window.showErrorMessage(`Error: No hyperpod clusters found`) + throw new Error(`Error: No hyperpod cluster found`) + } + const spaceMap: Map = new Map() + + for (const cluster of clusters) { + if (!cluster.eksClusterName) { + getLogger().warn(`HyperPod cluster ${cluster.clusterName} does not have an EKS cluster`) + continue + } + + const eksCommand = new DescribeClusterCommand({ + name: cluster.eksClusterName, + }) + const eksResponse = await this.eksClient.send(eksCommand) + if (!eksResponse) { + getLogger().warn(`Error: Invalid response`) + continue + } + + const eksCluster = eksResponse.cluster + if (!eksCluster) { + getLogger().warn( + `Error: EKS cluster ${cluster.eksClusterName} not found in region ${cluster.regionCode}` + ) + continue + } + + let kcClient = this.getKubectlClient(cluster.clusterName) + if (!kcClient) { + kcClient = new KubectlClient(eksCluster, cluster) + this.kubectlClients.set(cluster.clusterName, kcClient) + } + const spacesPerCluster = await kcClient.getSpacesForCluster(eksCluster) + if (!spacesPerCluster) { + getLogger().warn(`Error: No spaces found in eks cluster ${cluster.eksClusterName}`) + continue + } + + for (const devSpace of spacesPerCluster) { + const key = this.getWorkspaceKey(devSpace) + spaceMap.set(key, { cluster, devSpace }) + } + } + return spaceMap + } catch (error) { + void vscode.window.showErrorMessage(`Error: No workspaces listed`) + throw new Error(`Error: No workspaces listed`) + } + } + + public async updateChildren(): Promise { + this.allSpaces = await this.listSpaces() + this.clusterNamespaces.clear() + + for (const [_, { devSpace }] of this.allSpaces) { + const filterKey = this.getClusterNamespaceKey(devSpace) + this.clusterNamespaces.set(filterKey, devSpace) + } + + const filterSpaces = new Map(this.allSpaces) + this.callerIdentity = await this.stsClient.getCallerIdentity() + + const selectedClusterNamespaces = await this.getSelectedClusterNamespaces() + + for (const [key, { devSpace }] of this.allSpaces) { + const filterKey = this.getClusterNamespaceKey(devSpace) + if (!selectedClusterNamespaces.has(filterKey)) { + filterSpaces.delete(key) + } + } + + updateInPlace( + this.hyperpodDevSpaceNodes, + filterSpaces.keys(), + (key: string) => this.hyperpodDevSpaceNodes.get(key)!.updateWorkspace(), + (key: string) => + new SagemakerDevSpaceNode( + this, + filterSpaces.get(key)!.devSpace, + filterSpaces.get(key)!.cluster, + this.regionCode + ) + ) + } + + public trackPendingNode(devSpaceKey: string) { + this.pollingSet.add(devSpaceKey) + } + + private async updatePendingNodes() { + for (const key of this.pollingSet) { + const pendingDevSpaceNode = this.getHyperpodNode(key) + await this.updatePendingHyperpodSpaceNode(pendingDevSpaceNode) + } + } + + private async updatePendingHyperpodSpaceNode(devSpaceNode: SagemakerDevSpaceNode) { + await devSpaceNode.updateWorkspaceStatus() + if (!devSpaceNode.isPending()) { + this.pollingSet.delete(devSpaceNode.getDevSpaceKey()) + await devSpaceNode.refreshNode() + } + } + + public getKubectlClient(clusterName: string): KubectlClient | undefined { + return this.kubectlClients.get(clusterName) + } + + private getHyperpodNode(key: string): SagemakerDevSpaceNode { + const devSpaceNode = this.hyperpodDevSpaceNodes.get(key) + if (devSpaceNode) { + return devSpaceNode + } else { + throw new Error(`[Hyperpod] Devspace ${key} from polling set not found`) + } + } + + public getClusterNamespaceKey(space: HyperpodDevSpace): string { + return `${space.cluster}-${space.namespace}` + } + + public getWorkspaceKey(space: HyperpodDevSpace): string { + return `${space.cluster}-${space.namespace}-${space.name}` + } + + public async getDefaultSelectedClusterNamespaces(): Promise { + return [...this.clusterNamespaces.keys()] + } + + public async getSelectedClusterNamespaces(): Promise> { + const selectedClusterNamespacesByRegionMap = new Map( + globals.globalState.get( + SagemakerConstants.SelectedClusterNamespacesState, + [] + ) + ) + + const selectedClusterNamespacesMap = new Map(selectedClusterNamespacesByRegionMap.get(this.regionCode)) + const defaultSelectedClusterNamespaces = await this.getDefaultSelectedClusterNamespaces() + const cachedClusterNamespaces = selectedClusterNamespacesMap.get(this.callerIdentity.Arn || '') + + if (cachedClusterNamespaces && cachedClusterNamespaces.length > 0) { + return new Set(cachedClusterNamespaces) + } else { + return new Set(defaultSelectedClusterNamespaces) + } + } + + public saveSelectedClusterNamespaces(selectedClusterNamespaces: string[]) { + const selectedClusterNamespacesByRegionMap = new Map( + globals.globalState.get( + SagemakerConstants.SelectedClusterNamespacesState, + [] + ) + ) + + const selectedClusterNamespacesMap = new Map(selectedClusterNamespacesByRegionMap.get(this.regionCode)) + + if (this.callerIdentity.Arn) { + selectedClusterNamespacesMap?.set(this.callerIdentity.Arn, selectedClusterNamespaces) + selectedClusterNamespacesByRegionMap?.set(this.regionCode, [...selectedClusterNamespacesMap]) + + globals.globalState.tryUpdate(SagemakerConstants.SelectedClusterNamespacesState, [ + ...selectedClusterNamespacesByRegionMap, + ]) + } + } +} diff --git a/packages/core/src/awsService/sagemaker/explorer/sagemakerParentNode.ts b/packages/core/src/awsService/sagemaker/explorer/sagemakerParentNode.ts index dd445f344fb..e58f168aad1 100644 --- a/packages/core/src/awsService/sagemaker/explorer/sagemakerParentNode.ts +++ b/packages/core/src/awsService/sagemaker/explorer/sagemakerParentNode.ts @@ -4,211 +4,32 @@ */ import * as vscode from 'vscode' -import { GetCallerIdentityResponse } from 'aws-sdk/clients/sts' -import { DescribeDomainResponse } from '@amzn/sagemaker-client' -import { SagemakerClient, SagemakerSpaceApp } from '../../../shared/clients/sagemaker' -import { DefaultStsClient } from '../../../shared/clients/stsClient' -import globals from '../../../shared/extensionGlobals' +import { SagemakerClient } from '../../../shared/clients/sagemaker' import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' -import { PlaceholderNode } from '../../../shared/treeview/nodes/placeholderNode' -import { makeChildrenNodes } from '../../../shared/treeview/utils' -import { updateInPlace } from '../../../shared/utilities/collectionUtils' -import { isRemoteWorkspace } from '../../../shared/vscode/env' -import { SagemakerConstants } from './constants' -import { SagemakerSpaceNode } from './sagemakerSpaceNode' -import { getDomainSpaceKey, getDomainUserProfileKey, getSpaceAppsForUserProfile } from '../utils' -import { PollingSet } from '../../../shared/utilities/pollingSet' -import { getRemoteAppMetadata } from '../remoteUtils' +import { SagemakerStudioNode } from './sagemakerStudioNode' +import { SagemakerHyperpodNode } from './sagemakerHyperpodNode' export const parentContextValue = 'awsSagemakerParentNode' -export type SelectedDomainUsers = [string, string[]][] -export type SelectedDomainUsersByRegion = [string, SelectedDomainUsers][] - -export interface UserProfileMetadata { - domain: DescribeDomainResponse -} export class SagemakerParentNode extends AWSTreeNodeBase { - protected sagemakerSpaceNodes: Map - protected stsClient: DefaultStsClient public override readonly contextValue: string = parentContextValue - domainUserProfiles: Map = new Map() - spaceApps: Map = new Map() - callerIdentity: GetCallerIdentityResponse = {} - public readonly pollingSet: PollingSet = new PollingSet(5000, this.updatePendingNodes.bind(this)) + private studioNode: SagemakerStudioNode + private hyperpodNode: SagemakerHyperpodNode public constructor( public override readonly regionCode: string, protected readonly sagemakerClient: SagemakerClient ) { super('SageMaker AI', vscode.TreeItemCollapsibleState.Collapsed) - this.sagemakerSpaceNodes = new Map() - this.stsClient = new DefaultStsClient(regionCode) + this.studioNode = new SagemakerStudioNode(regionCode, sagemakerClient) + this.hyperpodNode = new SagemakerHyperpodNode(regionCode, sagemakerClient) } public override async getChildren(): Promise { - const result = await makeChildrenNodes({ - getChildNodes: async () => { - await this.updateChildren() - return [...this.sagemakerSpaceNodes.values()] - }, - getNoChildrenPlaceholderNode: async () => new PlaceholderNode(this, SagemakerConstants.PlaceHolderMessage), - sort: (nodeA, nodeB) => nodeA.name.localeCompare(nodeB.name), - }) - - return result - } - - public trackPendingNode(domainSpaceKey: string) { - this.pollingSet.add(domainSpaceKey) - } - - private async updatePendingNodes() { - for (const spaceKey of this.pollingSet.values()) { - const childNode = this.getSpaceNodes(spaceKey) - await this.updatePendingSpaceNode(childNode) - } - } - - private async updatePendingSpaceNode(node: SagemakerSpaceNode) { - await node.updateSpaceAppStatus() - if (!node.isPending()) { - this.pollingSet.delete(node.DomainSpaceKey) - await node.refreshNode() - } - } - - public getSpaceNodes(spaceKey: string): SagemakerSpaceNode { - const childNode = this.sagemakerSpaceNodes.get(spaceKey) - if (childNode) { - return childNode - } else { - throw new Error(`Node with id ${spaceKey} from polling set not found`) - } - } - - public async getLocalSelectedDomainUsers(): Promise { - /** - * By default, filter userProfileNames that match the detected IAM user, IAM assumed role - * session name, or Identity Center username - * */ - const iamMatches = - this.callerIdentity.Arn?.match(SagemakerConstants.IamUserArnRegex) || - this.callerIdentity.Arn?.match(SagemakerConstants.IamSessionArnRegex) - const idcMatches = this.callerIdentity.Arn?.match(SagemakerConstants.IdentityCenterArnRegex) - - const matches = - /** - * Only filter IAM users / assumed-role sessions if the user has enabled this option - * Or filter Identity Center username if user is authenticated via IdC - * */ - iamMatches && vscode.workspace.getConfiguration().get(SagemakerConstants.EnableIdentityFilteringSetting) - ? iamMatches - : idcMatches - ? idcMatches - : undefined - - const userProfilePrefix = - matches && matches.length >= 2 - ? `${matches[1].replaceAll(SagemakerConstants.SpecialCharacterRegex, '-')}-` - : '' - - return getSpaceAppsForUserProfile([...this.spaceApps.values()], userProfilePrefix) - } - - public async getRemoteSelectedDomainUsers(): Promise { - const remoteAppMetadata = await getRemoteAppMetadata() - return getSpaceAppsForUserProfile( - [...this.spaceApps.values()], - remoteAppMetadata.UserProfileName, - remoteAppMetadata.DomainId - ) - } - - public async getDefaultSelectedDomainUsers(): Promise { - if (isRemoteWorkspace()) { - return this.getRemoteSelectedDomainUsers() - } else { - return this.getLocalSelectedDomainUsers() - } - } - - public async getSelectedDomainUsers(): Promise> { - const selectedDomainUsersByRegionMap = new Map( - globals.globalState.get(SagemakerConstants.SelectedDomainUsersState, []) - ) - - const selectedDomainUsersMap = new Map(selectedDomainUsersByRegionMap.get(this.regionCode)) - const defaultSelectedDomainUsers = await this.getDefaultSelectedDomainUsers() - const cachedDomainUsers = selectedDomainUsersMap.get(this.callerIdentity.Arn || '') - - if (cachedDomainUsers && cachedDomainUsers.length > 0) { - return new Set(cachedDomainUsers) - } else { - return new Set(defaultSelectedDomainUsers) - } - } - - public saveSelectedDomainUsers(selectedDomainUsers: string[]) { - const selectedDomainUsersByRegionMap = new Map( - globals.globalState.get(SagemakerConstants.SelectedDomainUsersState, []) - ) - - const selectedDomainUsersMap = new Map(selectedDomainUsersByRegionMap.get(this.regionCode)) - - if (this.callerIdentity.Arn) { - selectedDomainUsersMap?.set(this.callerIdentity.Arn, selectedDomainUsers) - selectedDomainUsersByRegionMap?.set(this.regionCode, [...selectedDomainUsersMap]) - - globals.globalState.tryUpdate(SagemakerConstants.SelectedDomainUsersState, [ - ...selectedDomainUsersByRegionMap, - ]) - } - } - - public async updateChildren(): Promise { - const [spaceApps, domains] = await this.sagemakerClient.fetchSpaceAppsAndDomains() - this.spaceApps = spaceApps - - this.callerIdentity = await this.stsClient.getCallerIdentity() - const selectedDomainUsers = await this.getSelectedDomainUsers() - this.domainUserProfiles.clear() - - for (const app of spaceApps.values()) { - const domainId = app.DomainId - const userProfile = app.OwnershipSettingsSummary?.OwnerUserProfileName - if (!domainId || !userProfile) { - continue - } - - // populate domainUserProfiles for filtering - const domainUserProfileKey = getDomainUserProfileKey(domainId, userProfile) - const domainSpaceKey = getDomainSpaceKey(domainId, app.SpaceName || '') - - this.domainUserProfiles.set(domainUserProfileKey, { - domain: domains.get(domainId) as DescribeDomainResponse, - }) - - if (!selectedDomainUsers.has(domainUserProfileKey) && app.SpaceName) { - spaceApps.delete(domainSpaceKey) - continue - } - } - - updateInPlace( - this.sagemakerSpaceNodes, - spaceApps.keys(), - (key) => this.sagemakerSpaceNodes.get(key)!.updateSpace(spaceApps.get(key)!), - (key) => new SagemakerSpaceNode(this, this.sagemakerClient, this.regionCode, spaceApps.get(key)!) - ) - } - - public async clearChildren() { - this.sagemakerSpaceNodes = new Map() + return [this.studioNode, this.hyperpodNode] } - public async refreshNode(): Promise { - await this.clearChildren() - await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', this) + public getStudioNode(): SagemakerStudioNode { + return this.studioNode } } diff --git a/packages/core/src/awsService/sagemaker/explorer/sagemakerSpaceNode.ts b/packages/core/src/awsService/sagemaker/explorer/sagemakerSpaceNode.ts index 6151224a510..669b80497f7 100644 --- a/packages/core/src/awsService/sagemaker/explorer/sagemakerSpaceNode.ts +++ b/packages/core/src/awsService/sagemaker/explorer/sagemakerSpaceNode.ts @@ -4,156 +4,78 @@ */ import * as vscode from 'vscode' -import { AppType } from '@aws-sdk/client-sagemaker' import { SagemakerClient, SagemakerSpaceApp } from '../../../shared/clients/sagemaker' import { AWSResourceNode } from '../../../shared/treeview/nodes/awsResourceNode' import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' -import { SagemakerParentNode } from './sagemakerParentNode' -import { generateSpaceStatus } from '../utils' -import { getIcon } from '../../../shared/icons' +import { SagemakerStudioNode } from './sagemakerStudioNode' import { getLogger } from '../../../shared/logger/logger' +import { SagemakerUnifiedStudioSpaceNode } from '../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode' +import { SagemakerSpace } from '../sagemakerSpace' export class SagemakerSpaceNode extends AWSTreeNodeBase implements AWSResourceNode { + private smSpace: SagemakerSpace public constructor( - public readonly parent: SagemakerParentNode, + public readonly parent: SagemakerStudioNode, public readonly client: SagemakerClient, public override readonly regionCode: string, public readonly spaceApp: SagemakerSpaceApp ) { super('') + this.smSpace = new SagemakerSpace(this.client, this.regionCode, this.spaceApp) this.updateSpace(spaceApp) - this.contextValue = this.getContext() + this.contextValue = this.smSpace.getContext() } public updateSpace(spaceApp: SagemakerSpaceApp) { - this.setSpaceStatus(spaceApp.Status ?? '', spaceApp.App?.Status ?? '') - this.label = this.buildLabel() - this.description = this.buildDescription() - this.tooltip = new vscode.MarkdownString(this.buildTooltip()) - this.iconPath = this.getAppIcon() - + this.smSpace.updateSpace(spaceApp) + this.updateFromSpace() if (this.isPending()) { this.parent.trackPendingNode(this.DomainSpaceKey) } } - public setSpaceStatus(spaceStatus: string, appStatus: string) { - this.spaceApp.Status = spaceStatus - if (this.spaceApp.App) { - this.spaceApp.App.Status = appStatus - } + private updateFromSpace() { + this.label = this.smSpace.label + this.description = this.smSpace.description + this.tooltip = this.smSpace.tooltip + this.iconPath = this.smSpace.iconPath + this.contextValue = this.smSpace.contextValue } public isPending(): boolean { - return this.getStatus() !== 'Running' && this.getStatus() !== 'Stopped' + return this.smSpace.isPending() } public getStatus(): string { - return generateSpaceStatus(this.spaceApp.Status, this.spaceApp.App?.Status) + return this.smSpace.getStatus() } public async getAppStatus() { - const app = await this.client.describeApp({ - DomainId: this.spaceApp.DomainId, - AppName: this.spaceApp.App?.AppName, - AppType: this.spaceApp.SpaceSettingsSummary?.AppType, - SpaceName: this.spaceApp.SpaceName, - }) - - return app.Status ?? 'Unknown' + return this.smSpace.getAppStatus() } public get name(): string { - return this.spaceApp.SpaceName ?? `(no name)` + return this.smSpace.name } public get arn(): string { - return 'placeholder-arn' + return this.smSpace.arn } public async getAppArn() { - const appDetails = await this.client.describeApp({ - DomainId: this.spaceApp.DomainId, - AppName: this.spaceApp.App?.AppName, - AppType: this.spaceApp.SpaceSettingsSummary?.AppType, - SpaceName: this.spaceApp.SpaceName, - }) - - return appDetails.AppArn + return this.smSpace.getAppArn() } public async getSpaceArn() { - const appDetails = await this.client.describeSpace({ - DomainId: this.spaceApp.DomainId, - SpaceName: this.spaceApp.SpaceName, - }) - - return appDetails.SpaceArn + return this.smSpace.getSpaceArn() } public async updateSpaceAppStatus() { - const space = await this.client.describeSpace({ - DomainId: this.spaceApp.DomainId, - SpaceName: this.spaceApp.SpaceName, - }) - - const app = await this.client.describeApp({ - DomainId: this.spaceApp.DomainId, - AppName: this.spaceApp.App?.AppName, - AppType: this.spaceApp.SpaceSettingsSummary?.AppType, - SpaceName: this.spaceApp.SpaceName, - }) - - this.updateSpace({ - ...space, - App: app, - DomainSpaceKey: this.spaceApp.DomainSpaceKey, - }) - } - - private buildLabel(): string { - const status = generateSpaceStatus(this.spaceApp.Status, this.spaceApp.App?.Status) - return `${this.name} (${status})` - } - - private buildDescription(): string { - return `${this.spaceApp.SpaceSharingSettingsSummary?.SharingType ?? 'Unknown'} space` - } - private buildTooltip() { - const spaceName = this.spaceApp?.SpaceName ?? '-' - const appType = this.spaceApp?.SpaceSettingsSummary?.AppType ?? '-' - const domainId = this.spaceApp?.DomainId ?? '-' - const owner = this.spaceApp?.OwnershipSettingsSummary?.OwnerUserProfileName ?? '-' - - return `**Space:** ${spaceName} \n\n**Application:** ${appType} \n\n**Domain ID:** ${domainId} \n\n**User Profile:** ${owner}` - } - - private getAppIcon() { - if (this.spaceApp.SpaceSettingsSummary?.AppType === AppType.CodeEditor) { - return getIcon('aws-sagemaker-code-editor') - } - - if (this.spaceApp.SpaceSettingsSummary?.AppType === AppType.JupyterLab) { - return getIcon('aws-sagemaker-jupyter-lab') - } - } - - private getContext() { - const status = this.getStatus() - if (status === 'Running' && this.spaceApp.SpaceSettingsSummary?.RemoteAccess === 'ENABLED') { - return 'awsSagemakerSpaceRunningRemoteEnabledNode' - } else if (status === 'Running' && this.spaceApp.SpaceSettingsSummary?.RemoteAccess === 'DISABLED') { - return 'awsSagemakerSpaceRunningRemoteDisabledNode' - } else if (status === 'Stopped' && this.spaceApp.SpaceSettingsSummary?.RemoteAccess === 'ENABLED') { - return 'awsSagemakerSpaceStoppedRemoteEnabledNode' - } else if ( - status === 'Stopped' && - (!this.spaceApp.SpaceSettingsSummary?.RemoteAccess || - this.spaceApp.SpaceSettingsSummary?.RemoteAccess === 'DISABLED') - ) { - return 'awsSagemakerSpaceStoppedRemoteDisabledNode' + await this.smSpace.updateSpaceAppStatus() + this.updateFromSpace() + if (this.isPending()) { + this.parent.trackPendingNode(this.DomainSpaceKey) } - return 'awsSagemakerSpaceNode' } public get DomainSpaceKey(): string { @@ -166,13 +88,15 @@ export class SagemakerSpaceNode extends AWSTreeNodeBase implements AWSResourceNo } } -export async function tryRefreshNode(node?: SagemakerSpaceNode) { +export async function tryRefreshNode(node?: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode) { if (node) { try { // For SageMaker spaces, refresh just the individual space node to avoid expensive // operation of refreshing all spaces in the domain await node.updateSpaceAppStatus() - await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', node) + node instanceof SagemakerSpaceNode + ? await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', node) + : await node.refreshNode() } catch (e) { getLogger().error('refreshNode failed: %s', (e as Error).message) } diff --git a/packages/core/src/awsService/sagemaker/explorer/sagemakerStudioNode.ts b/packages/core/src/awsService/sagemaker/explorer/sagemakerStudioNode.ts new file mode 100644 index 00000000000..be7b50238fe --- /dev/null +++ b/packages/core/src/awsService/sagemaker/explorer/sagemakerStudioNode.ts @@ -0,0 +1,206 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { GetCallerIdentityResponse } from 'aws-sdk/clients/sts' +import { DescribeDomainResponse } from '@amzn/sagemaker-client' +import { SagemakerClient, SagemakerSpaceApp } from '../../../shared/clients/sagemaker' +import { DefaultStsClient } from '../../../shared/clients/stsClient' +import globals from '../../../shared/extensionGlobals' +import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' +import { PlaceholderNode } from '../../../shared/treeview/nodes/placeholderNode' +import { makeChildrenNodes } from '../../../shared/treeview/utils' +import { updateInPlace } from '../../../shared/utilities/collectionUtils' +import { isRemoteWorkspace } from '../../../shared/vscode/env' +import { SagemakerConstants } from './constants' +import { SagemakerSpaceNode } from './sagemakerSpaceNode' +import { getDomainSpaceKey, getDomainUserProfileKey, getSpaceAppsForUserProfile } from '../utils' +import { PollingSet } from '../../../shared/utilities/pollingSet' +import { getRemoteAppMetadata } from '../remoteUtils' + +export type SelectedDomainUsers = [string, string[]][] +export type SelectedDomainUsersByRegion = [string, SelectedDomainUsers][] + +export interface UserProfileMetadata { + domain: DescribeDomainResponse +} + +export const studioContextValue = 'awsSagemakerStudioNode' + +export class SagemakerStudioNode extends AWSTreeNodeBase { + protected sagemakerSpaceNodes: Map + protected stsClient: DefaultStsClient + public override readonly contextValue: string = studioContextValue + domainUserProfiles: Map = new Map() + spaceApps: Map = new Map() + callerIdentity: GetCallerIdentityResponse = {} + public readonly pollingSet: PollingSet = new PollingSet(5000, this.updatePendingNodes.bind(this)) + + public constructor( + public override readonly regionCode: string, + protected readonly sagemakerClient: SagemakerClient + ) { + super('Studio', vscode.TreeItemCollapsibleState.Collapsed) + this.sagemakerSpaceNodes = new Map() + this.stsClient = new DefaultStsClient(regionCode) + } + + public override async getChildren(): Promise { + const result = await makeChildrenNodes({ + getChildNodes: async () => { + await this.updateChildren() + return [...this.sagemakerSpaceNodes.values()] + }, + getNoChildrenPlaceholderNode: async () => new PlaceholderNode(this, SagemakerConstants.PlaceHolderMessage), + sort: (nodeA, nodeB) => nodeA.name.localeCompare(nodeB.name), + }) + + return result + } + + public trackPendingNode(domainSpaceKey: string) { + this.pollingSet.add(domainSpaceKey) + } + + private async updatePendingNodes() { + for (const spaceKey of this.pollingSet.values()) { + const childNode = this.getSpaceNodes(spaceKey) + await this.updatePendingSpaceNode(childNode) + } + } + + private async updatePendingSpaceNode(node: SagemakerSpaceNode) { + await node.updateSpaceAppStatus() + if (!node.isPending()) { + this.pollingSet.delete(node.DomainSpaceKey) + await node.refreshNode() + } + } + + public getSpaceNodes(spaceKey: string): SagemakerSpaceNode { + const childNode = this.sagemakerSpaceNodes.get(spaceKey) + if (childNode) { + return childNode + } else { + throw new Error(`Node with id ${spaceKey} from polling set not found`) + } + } + + public async getLocalSelectedDomainUsers(): Promise { + const iamMatches = + this.callerIdentity.Arn?.match(SagemakerConstants.IamUserArnRegex) || + this.callerIdentity.Arn?.match(SagemakerConstants.IamSessionArnRegex) + const idcMatches = this.callerIdentity.Arn?.match(SagemakerConstants.IdentityCenterArnRegex) + + const matches = + iamMatches && vscode.workspace.getConfiguration().get(SagemakerConstants.EnableIdentityFilteringSetting) + ? iamMatches + : idcMatches + ? idcMatches + : undefined + + const userProfilePrefix = + matches && matches.length >= 2 + ? `${matches[1].replaceAll(SagemakerConstants.SpecialCharacterRegex, '-')}-` + : '' + + return getSpaceAppsForUserProfile([...this.spaceApps.values()], userProfilePrefix) + } + + public async getRemoteSelectedDomainUsers(): Promise { + const remoteAppMetadata = await getRemoteAppMetadata() + return getSpaceAppsForUserProfile( + [...this.spaceApps.values()], + remoteAppMetadata.UserProfileName, + remoteAppMetadata.DomainId + ) + } + + public async getDefaultSelectedDomainUsers(): Promise { + if (isRemoteWorkspace()) { + return this.getRemoteSelectedDomainUsers() + } else { + return this.getLocalSelectedDomainUsers() + } + } + + public async getSelectedDomainUsers(): Promise> { + const selectedDomainUsersByRegionMap = new Map( + globals.globalState.get(SagemakerConstants.SelectedDomainUsersState, []) + ) + + const selectedDomainUsersMap = new Map(selectedDomainUsersByRegionMap.get(this.regionCode)) + const defaultSelectedDomainUsers = await this.getDefaultSelectedDomainUsers() + const cachedDomainUsers = selectedDomainUsersMap.get(this.callerIdentity.Arn || '') + + if (cachedDomainUsers && cachedDomainUsers.length > 0) { + return new Set(cachedDomainUsers) + } else { + return new Set(defaultSelectedDomainUsers) + } + } + + public saveSelectedDomainUsers(selectedDomainUsers: string[]) { + const selectedDomainUsersByRegionMap = new Map( + globals.globalState.get(SagemakerConstants.SelectedDomainUsersState, []) + ) + + const selectedDomainUsersMap = new Map(selectedDomainUsersByRegionMap.get(this.regionCode)) + + if (this.callerIdentity.Arn) { + selectedDomainUsersMap?.set(this.callerIdentity.Arn, selectedDomainUsers) + selectedDomainUsersByRegionMap?.set(this.regionCode, [...selectedDomainUsersMap]) + + globals.globalState.tryUpdate(SagemakerConstants.SelectedDomainUsersState, [ + ...selectedDomainUsersByRegionMap, + ]) + } + } + + public async updateChildren(): Promise { + const [spaceApps, domains] = await this.sagemakerClient.fetchSpaceAppsAndDomains() + this.spaceApps = spaceApps + + this.callerIdentity = await this.stsClient.getCallerIdentity() + const selectedDomainUsers = await this.getSelectedDomainUsers() + this.domainUserProfiles.clear() + + for (const app of spaceApps.values()) { + const domainId = app.DomainId + const userProfile = app.OwnershipSettingsSummary?.OwnerUserProfileName + if (!domainId || !userProfile) { + continue + } + + const domainUserProfileKey = getDomainUserProfileKey(domainId, userProfile) + const domainSpaceKey = getDomainSpaceKey(domainId, app.SpaceName || '') + + this.domainUserProfiles.set(domainUserProfileKey, { + domain: domains.get(domainId) as DescribeDomainResponse, + }) + + if (!selectedDomainUsers.has(domainUserProfileKey) && app.SpaceName) { + spaceApps.delete(domainSpaceKey) + continue + } + } + + updateInPlace( + this.sagemakerSpaceNodes, + spaceApps.keys(), + (key) => this.sagemakerSpaceNodes.get(key)!.updateSpace(spaceApps.get(key)!), + (key) => new SagemakerSpaceNode(this as any, this.sagemakerClient, this.regionCode, spaceApps.get(key)!) + ) + } + + public async clearChildren() { + this.sagemakerSpaceNodes = new Map() + } + + public async refreshNode(): Promise { + await this.clearChildren() + await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', this) + } +} diff --git a/packages/core/src/awsService/sagemaker/hyperpodCommands.ts b/packages/core/src/awsService/sagemaker/hyperpodCommands.ts new file mode 100644 index 00000000000..454bcc49c1d --- /dev/null +++ b/packages/core/src/awsService/sagemaker/hyperpodCommands.ts @@ -0,0 +1,182 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as nls from 'vscode-nls' +import { getLogger } from '../../shared/logger/logger' +import { isRemoteWorkspace } from '../../shared/vscode/env' +import { SagemakerDevSpaceNode } from './explorer/sagemakerDevSpaceNode' +import { showConfirmationMessage } from '../../shared/utilities/messages' +import { SagemakerConstants } from './explorer/constants' +import { SagemakerHyperpodNode } from './explorer/sagemakerHyperpodNode' + +const localize = nls.loadMessageBundle() + +export async function openHyperPodRemoteConnection(node: SagemakerDevSpaceNode): Promise { + await startHyperpodSpaceCommand(node) + await waitForDevSpaceRunning(node) + await connectToHyperPodDevSpace(node) +} + +async function waitForDevSpaceRunning(node: SagemakerDevSpaceNode): Promise { + const kubectlClient = node.getParent().getKubectlClient(node.hpCluster.clusterName) + if (!kubectlClient) { + getLogger().error(`No kubectlClient available for cluster: ${node.hpCluster.clusterName}`) + return + } + const timeout = 5 * 60 * 1000 // 5 minutes + const startTime = Date.now() + + while (Date.now() - startTime < timeout) { + const status = await kubectlClient.getHyperpodSpaceStatus(node.devSpace) + if (status === 'Running') { + return + } + await new Promise((resolve) => setTimeout(resolve, 5000)) + } + + throw new Error('Timeout waiting for dev space to reach Running status') +} + +export async function connectToHyperPodDevSpace(node: SagemakerDevSpaceNode): Promise { + const logger = getLogger() + + if (isRemoteWorkspace()) { + void vscode.window.showErrorMessage( + 'Cannot connect to HyperPod from a remote workspace. Please use a local VS Code instance.' + ) + return + } + + try { + const kubectlClient = node.getParent().getKubectlClient(node.hpCluster.clusterName) + if (!kubectlClient) { + logger.error(`No kubectlClient available for cluster: ${node.hpCluster.clusterName}`) + return + } + const response = await kubectlClient.createWorkspaceConnection(node.devSpace) + getLogger().debug(`HyperPod connection response: &O`, response) + await vscode.env.openExternal(vscode.Uri.parse(response.url)) + void vscode.window.showInformationMessage(`Started connection to HyperPod dev space: ${node.devSpace.name}`) + } catch (error) { + logger.error(`Failed to connect to HyperPod dev space: ${error}`) + void vscode.window.showErrorMessage( + `Failed to connect to HyperPod dev space: ${error instanceof Error ? error.message : String(error)}` + ) + } +} + +export async function startHyperpodSpaceCommand(node: SagemakerDevSpaceNode): Promise { + if (node.devSpace.status === 'Invalid') { + void vscode.window.showErrorMessage(`Error: Cannot start an invalid space`) + throw new Error(`Error: Cannot start an invalid space`) + } + if (node.devSpace.status === 'Error') { + void vscode.window.showErrorMessage(`Error: Cannot start space until resolved`) + throw new Error(`Error: Cannot start space until resolved`) + } + if (node.devSpace.status === 'Running') { + return + } + // Set transitional state immediately + node.devSpace.status = 'Starting' + node.contextValue = 'awsSagemakerHyperpodDevSpaceTransitionalNode' + await node.refreshNode() + + const kc = node.getParent().getKubectlClient(node.hpCluster.clusterName) + if (!kc) { + getLogger().error(`Failed to start space (${node.devSpace.name}) due to unavailable kubectl client`) + return + } + await kc.startHyperpodDevSpace(node) +} + +export async function stopHyperPodSpaceCommand(node: SagemakerDevSpaceNode): Promise { + const confirmed = await showConfirmationMessage({ + prompt: `You are about to stop this space. Any active resource will also be stopped. Are you sure you want to stop the space?`, + confirm: 'Stop Space', + cancel: 'Cancel', + type: 'warning', + }) + + if (!confirmed) { + return + } + + if (node.devSpace.status === 'Error') { + void vscode.window.showErrorMessage(`Error: Cannot stop space until resolved`) + throw new Error(`Error: Cannot stop space until resolved`) + } + + // Set transitional state immediately + node.devSpace.status = 'Stopping' + node.contextValue = 'awsSagemakerHyperpodDevSpaceTransitionalNode' + await node.refreshNode() + + const kc = node.getParent().getKubectlClient(node.hpCluster.clusterName) + if (!kc) { + getLogger().error(`Failed to start space (${node.devSpace.name}) due to unavailable kubectl client`) + return + } + await kc.stopHyperpodDevSpace(node) +} + +export async function filterDevSpacesByNamespaceCluster(hpNode: SagemakerHyperpodNode): Promise { + if (hpNode.clusterNamespaces.size === 0) { + // if hyperpodNode has not been expanded, then devSpaceNodes will be empty + // if so, this will attempt to populate devSpaceNodes + await hpNode.updateChildren() + if (hpNode.clusterNamespaces.size === 0) { + getLogger().info(SagemakerConstants.NoDevSpaceToFilter) + void vscode.window.showInformationMessage(SagemakerConstants.NoDevSpaceToFilter) + return + } + } + + // Sort by EKS cluster name and namespace + const sortedClusterNamespaces = new Map( + [...hpNode.clusterNamespaces].sort((a, b) => { + const clusterA = a[1].cluster + const clusterB = b[1].cluster + const namespaceA = a[1].namespace + const namespaceB = b[1].namespace + + return clusterA.localeCompare(clusterB) || namespaceA.localeCompare(namespaceB) + }) + ) + + const previousSelection = await hpNode.getSelectedClusterNamespaces() + const items: (vscode.QuickPickItem & { key: string })[] = [] + + for (const [_, devSpace] of sortedClusterNamespaces) { + const filterKey = `${devSpace.cluster}-${devSpace.namespace}` + items.push({ + label: devSpace.namespace, + detail: `In cluster: ${devSpace.cluster}`, + picked: previousSelection.has(filterKey), + key: filterKey, + }) + } + + const placeholder = localize( + SagemakerConstants.FilterHyperpodPlaceholderKey, + SagemakerConstants.FilterHyperpodPlaceholderMessage + ) + const result = await vscode.window.showQuickPick(items, { + placeHolder: placeholder, + canPickMany: true, + matchOnDetail: true, + }) + + if (!result) { + return // User canceled + } + + const newSelection = result.map((r) => r.key) + if (newSelection.length !== previousSelection.size || newSelection.some((key) => !previousSelection.has(key))) { + hpNode.saveSelectedClusterNamespaces(newSelection) + await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', hpNode) + } +} diff --git a/packages/core/src/awsService/sagemaker/model.ts b/packages/core/src/awsService/sagemaker/model.ts index 20a667a0bfa..b1fc5259fc8 100644 --- a/packages/core/src/awsService/sagemaker/model.ts +++ b/packages/core/src/awsService/sagemaker/model.ts @@ -6,11 +6,12 @@ // Disabled: detached server files cannot import vscode. /* eslint-disable no-restricted-imports */ import * as vscode from 'vscode' -import { sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../../shared/extensions/ssh' +import { getSshConfigPath, sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../../shared/extensions/ssh' import { createBoundProcess, ensureDependencies } from '../../shared/remoteSession' import { SshConfig } from '../../shared/sshConfig' +import { Result } from '../../shared/utilities/result' import * as path from 'path' -import { persistLocalCredentials, persistSSMConnection } from './credentialMapping' +import { persistLocalCredentials, persistSmusProjectCreds, persistSSMConnection } from './credentialMapping' import * as os from 'os' import _ from 'lodash' import { fs } from '../../shared/fs/fs' @@ -21,14 +22,52 @@ import { DevSettings } from '../../shared/settings' import { ToolkitError } from '../../shared/errors' import { SagemakerSpaceNode } from './explorer/sagemakerSpaceNode' import { sleep } from '../../shared/utilities/timeoutUtils' +import { SagemakerUnifiedStudioSpaceNode } from '../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode' +import { SshConfigError, SshConfigErrorMessage } from './constants' +import globals from '../../shared/extensionGlobals' const logger = getLogger('sagemaker') -export async function tryRemoteConnection(node: SagemakerSpaceNode, ctx: vscode.ExtensionContext) { - const spaceArn = (await node.getSpaceArn()) as string - const remoteEnv = await prepareDevEnvConnection(spaceArn, ctx, 'sm_lc') +class HyperPodSshConfig extends SshConfig { + constructor( + sshPath: string, + private readonly hyperpodConnectPath: string + ) { + super(sshPath, 'hp_', 'hyperpod_connect') + } + protected override createSSHConfigSection(proxyCommand: string): string { + return ` +# Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode +Host hp_* + ForwardAgent yes + AddKeysToAgent yes + StrictHostKeyChecking accept-new + ProxyCommand '${this.hyperpodConnectPath}' '%h' + IdentitiesOnly yes +` + } + + public override async ensureValid() { + const proxyCommand = `'${this.hyperpodConnectPath}' '%h'` + const verifyHost = await this.verifySSHHost(proxyCommand) + if (verifyHost.isErr()) { + return verifyHost + } + return Result.ok() + } +} + +export async function tryRemoteConnection( + node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode, + ctx: vscode.ExtensionContext, + progress: vscode.Progress<{ message?: string; increment?: number }> +) { + const spaceArn = (await node.getSpaceArn()) as string + const isSMUS = node instanceof SagemakerUnifiedStudioSpaceNode + const remoteEnv = await prepareDevEnvConnection(spaceArn, ctx, 'sm_lc', isSMUS, node) try { + progress.report({ message: 'Opening remote session' }) await startVscodeRemote( remoteEnv.SessionProcess, remoteEnv.hostname, @@ -43,14 +82,26 @@ export async function tryRemoteConnection(node: SagemakerSpaceNode, ctx: vscode. } } +export function extractRegionFromStreamUrl(streamUrl: string): string { + const url = new URL(streamUrl) + const match = url.hostname.match(/^[^.]+\.([^.]+)\.amazonaws\.com$/) + if (!match) { + throw new Error(`Unable to get region from stream url: ${streamUrl}`) + } + return match[1] +} + export async function prepareDevEnvConnection( - appArn: string, + spaceArn: string, ctx: vscode.ExtensionContext, connectionType: string, + isSMUS: boolean, + node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode | undefined, session?: string, wsUrl?: string, token?: string, - domain?: string + domain?: string, + appType?: string ) { const remoteLogger = configureRemoteConnectionLogger() const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() @@ -66,36 +117,145 @@ export async function prepareDevEnvConnection( } const hostnamePrefix = connectionType - const hostname = `${hostnamePrefix}_${appArn.replace(/\//g, '__').replace(/:/g, '_._')}` - + let hostname: string + if (connectionType === 'sm_hp') { + hostname = `hp_${session}` + } else { + hostname = `${hostnamePrefix}_${spaceArn.replace(/\//g, '__').replace(/:/g, '_._')}` + } // save space credential mapping if (connectionType === 'sm_lc') { - await persistLocalCredentials(appArn) + if (!isSMUS) { + await persistLocalCredentials(spaceArn) + } else { + await persistSmusProjectCreds(spaceArn, node as SagemakerUnifiedStudioSpaceNode) + } } else if (connectionType === 'sm_dl') { - await persistSSMConnection(appArn, domain ?? '', session, wsUrl, token) + await persistSSMConnection(spaceArn, domain ?? '', session, wsUrl, token, appType, isSMUS) } - await startLocalServer(ctx) + // HyperPod doesn't need the local server (only for SageMaker Studio) + if (connectionType !== 'sm_hp') { + await startLocalServer(ctx) + } await removeKnownHost(hostname) - const sshConfig = new SshConfig(ssh, 'sm_', 'sagemaker_connect') + const hyperpodConnectPath = path.join(ctx.globalStorageUri.fsPath, 'hyperpod_connect') + + // Copy hyperpod_connect script if needed + if (connectionType === 'sm_hp') { + const sourceScriptPath = ctx.asAbsolutePath('resources/hyperpod_connect') + if (!(await fs.existsFile(hyperpodConnectPath))) { + try { + await fs.copy(sourceScriptPath, hyperpodConnectPath) + await fs.chmod(hyperpodConnectPath, 0o755) + logger.info(`Copied hyperpod_connect script to ${hyperpodConnectPath}`) + } catch (err) { + logger.error(`Failed to copy hyperpod_connect script: ${err}`) + } + } + } + + const sshConfig = + connectionType === 'sm_hp' + ? new HyperPodSshConfig(ssh, hyperpodConnectPath) + : new SshConfig(ssh, 'sm_', 'sagemaker_connect') const config = await sshConfig.ensureValid() if (config.isErr()) { const err = config.err() logger.error(`sagemaker: failed to add ssh config section: ${err.message}`) + + if (err instanceof ToolkitError && err.code === 'SshCheckFailed') { + const sshConfigPath = getSshConfigPath() + const openConfigButton = 'Open SSH Config' + const resp = await vscode.window.showErrorMessage( + SshConfigErrorMessage(), + { modal: true, detail: err.message }, + openConfigButton + ) + + if (resp === openConfigButton) { + void vscode.window.showTextDocument(vscode.Uri.file(sshConfigPath)) + } + + // Throw error to stop the connection flow + // User is already notified via modal above, downstream handlers check the error code + throw new ToolkitError('Unable to connect: SSH configuration contains errors', { + code: SshConfigError, + }) + } + + const logPrefix = connectionType === 'sm_hp' ? 'hyperpod' : 'sagemaker' + logger.error(`${logPrefix}: failed to add ssh config section: ${err.message}`) throw err } // set envirionment variables - const vars = getSmSsmEnv(ssm, path.join(ctx.globalStorageUri.fsPath, 'sagemaker-local-server-info.json')) + const vars: NodeJS.ProcessEnv = + connectionType === 'sm_hp' + ? await (async () => { + const logFileLocation = path.join(ctx.globalStorageUri.fsPath, 'hyperpod-connection.log') + const decodedWsUrl = + wsUrl + ?.replace(/'/g, "'") + .replace(/"/g, '"') + .replace(/&/g, '&') || '' + const decodedToken = + token + ?.replace(/'/g, "'") + .replace(/"/g, '"') + .replace(/&/g, '&') || '' + const region = decodedWsUrl ? extractRegionFromStreamUrl(decodedWsUrl) : '' + + const hyperPodEnv: NodeJS.ProcessEnv = { + AWS_REGION: region, + SESSION_ID: session || '', + STREAM_URL: decodedWsUrl, + TOKEN: decodedToken, + AWS_SSM_CLI: ssm, + DEBUG_LOG: '1', + LOG_FILE_LOCATION: logFileLocation, + } + + // Add AWS credentials + try { + const creds = await globals.awsContext.getCredentials() + if (creds) { + hyperPodEnv.AWS_ACCESS_KEY_ID = creds.accessKeyId + hyperPodEnv.AWS_SECRET_ACCESS_KEY = creds.secretAccessKey + if (creds.sessionToken) { + hyperPodEnv.AWS_SESSION_TOKEN = creds.sessionToken + } + logger.info('Added AWS credentials to environment') + } else { + logger.warn('No AWS credentials available for HyperPod connection') + } + } catch (err) { + logger.warn(`Failed to get AWS credentials: ${err}`) + } + + return { ...process.env, ...hyperPodEnv } + })() + : getSmSsmEnv(ssm, path.join(ctx.globalStorageUri.fsPath, 'sagemaker-local-server-info.json')) + logger.info(`connect script logs at ${vars.LOG_FILE_LOCATION}`) const envProvider = async () => { return { [sshAgentSocketVariable]: await startSshAgent(), ...vars } } const SessionProcess = createBoundProcess(envProvider).extend({ - onStdout: remoteLogger, - onStderr: remoteLogger, + onStdout: (data: string) => { + remoteLogger(data) + if (connectionType === 'sm_hp') { + getLogger().info(`[ProxyCommand stdout] ${data}`) + } + }, + onStderr: (data: string) => { + remoteLogger(data) + if (connectionType === 'sm_hp') { + getLogger().error(`[ProxyCommand stderr] ${data}`) + } + }, rejectOnErrorCode: true, }) @@ -215,7 +375,13 @@ export async function removeKnownHost(hostname: string): Promise { throw ToolkitError.chain(err, 'Failed to read known_hosts file') } - const updatedLines = lines.filter((line) => !line.split(' ')[0].split(',').includes(hostname)) + const updatedLines = lines.filter((line) => { + const entryHostname = line.split(' ')[0].split(',') + // Hostnames in the known_hosts file seem to be always lowercase, but keeping the case-sensitive check just in + // case. Originally we were only doing the case-sensitive check which caused users to get a host + // identification error when reconnecting to a Space after it was restarted. + return !entryHostname.includes(hostname) && !entryHostname.includes(hostname.toLowerCase()) + }) if (updatedLines.length !== lines.length) { try { diff --git a/packages/core/src/awsService/sagemaker/sagemakerSpace.ts b/packages/core/src/awsService/sagemaker/sagemakerSpace.ts new file mode 100644 index 00000000000..f4bcfdd952f --- /dev/null +++ b/packages/core/src/awsService/sagemaker/sagemakerSpace.ts @@ -0,0 +1,248 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as path from 'path' +import { AppType } from '@aws-sdk/client-sagemaker' +import { SagemakerClient, SagemakerSpaceApp } from '../../shared/clients/sagemaker' +import { getIcon, IconPath } from '../../shared/icons' +import { generateSpaceStatus, updateIdleFile, startMonitoringTerminalActivity, ActivityCheckInterval } from './utils' +import { UserActivity } from '../../shared/extensionUtilities' +import { getLogger } from '../../shared/logger/logger' +import { ToolkitError } from '../../shared/errors' +import { SpaceStatus, RemoteAccess } from './constants' + +const logger = getLogger('sagemaker') + +export class SagemakerSpace { + public label: string = '' + public contextValue: string = '' + public description?: string + private spaceApp: SagemakerSpaceApp + public tooltip?: vscode.MarkdownString + public iconPath?: IconPath + public refreshCallback?: () => Promise + + public constructor( + private readonly client: SagemakerClient, + public readonly regionCode: string, + spaceApp: SagemakerSpaceApp, + private readonly isSMUSSpace: boolean = false + ) { + this.spaceApp = spaceApp + this.updateSpace(spaceApp) + this.contextValue = this.getContext() + } + + public updateSpace(spaceApp: SagemakerSpaceApp) { + // Edge case when this.spaceApp.App is null, returned by ListApp API for a Space that is not connected to for over 24 hours + if (!this.spaceApp.App) { + this.spaceApp.App = spaceApp.App + } + this.setSpaceStatus(spaceApp.Status ?? '', spaceApp.App?.Status ?? '') + // Only update RemoteAccess property to minimize impact due to minor structural differences between variables + if (this.spaceApp.SpaceSettingsSummary && spaceApp.SpaceSettingsSummary?.RemoteAccess) { + this.spaceApp.SpaceSettingsSummary.RemoteAccess = spaceApp.SpaceSettingsSummary.RemoteAccess + } + this.label = this.buildLabel() + this.description = this.isSMUSSpace ? undefined : this.buildDescription() + this.tooltip = new vscode.MarkdownString(this.buildTooltip()) + this.iconPath = this.getAppIcon() + this.contextValue = this.getContext() + } + + public setSpaceStatus(spaceStatus: string, appStatus: string) { + this.spaceApp.Status = spaceStatus + if (this.spaceApp.App) { + this.spaceApp.App.Status = appStatus + } + } + + public isPending(): boolean { + return this.getStatus() !== SpaceStatus.RUNNING && this.getStatus() !== SpaceStatus.STOPPED + } + + public getStatus(): string { + return generateSpaceStatus(this.spaceApp.Status, this.spaceApp.App?.Status) + } + + public async getAppStatus() { + const app = await this.client.describeApp({ + DomainId: this.spaceApp.DomainId, + AppName: this.spaceApp.App?.AppName, + AppType: this.spaceApp.SpaceSettingsSummary?.AppType, + SpaceName: this.spaceApp.SpaceName, + }) + + return app.Status ?? 'Unknown' + } + + public get name(): string { + return this.spaceApp.SpaceName ?? `(no name)` + } + + public get arn(): string { + return 'placeholder-arn' + } + + // TODO: Verify this method is still needed to retrieve the app ARN or build based on provided details + public async getAppArn() { + const appDetails = await this.client.describeApp({ + DomainId: this.spaceApp.DomainId, + AppName: this.spaceApp.App?.AppName, + AppType: this.spaceApp?.SpaceSettingsSummary?.AppType, + SpaceName: this.spaceApp.SpaceName, + }) + + return appDetails.AppArn + } + + // TODO: Verify this method is still needed to retrieve the app ARN or build based on provided details + public async getSpaceArn() { + const spaceDetails = await this.client.describeSpace({ + DomainId: this.spaceApp.DomainId, + SpaceName: this.spaceApp.SpaceName, + }) + + return spaceDetails.SpaceArn + } + + public async updateSpaceAppStatus() { + const space = await this.client.describeSpace({ + DomainId: this.spaceApp.DomainId, + SpaceName: this.spaceApp.SpaceName, + }) + // get app using ListApps API, with given DomainId and SpaceName + const app = + this.spaceApp.DomainId && this.spaceApp.SpaceName + ? await this.client.listAppForSpace(this.spaceApp.DomainId, this.spaceApp.SpaceName) + : undefined + if (!app) { + logger.error( + `updateSpaceAppStatus: unable to get app, [DomainId: ${this.spaceApp.DomainId}], [SpaceName: ${this.spaceApp.SpaceName}]` + ) + throw new ToolkitError( + `Cannot update app status without [DomainId: ${this.spaceApp.DomainId} and SpaceName: ${this.spaceApp.SpaceName}]` + ) + } + + // AWS DescribeSpace API returns full details with property names like 'SpaceSettings' + // but our internal SagemakerSpaceApp type expects 'SpaceSettingsSummary' (from ListSpaces API) + // We destructure and rename properties to maintain type compatibility + const { + SpaceSettings: spaceSettingsSummary, + OwnershipSettings: ownershipSettingsSummary, + SpaceSharingSettings: spaceSharingSettingsSummary, + ...spaceDetails + } = space + this.updateSpace({ + SpaceSettingsSummary: spaceSettingsSummary, + OwnershipSettingsSummary: ownershipSettingsSummary, + SpaceSharingSettingsSummary: spaceSharingSettingsSummary, + ...spaceDetails, + App: app, + DomainSpaceKey: this.spaceApp.DomainSpaceKey, + }) + } + + public buildLabel(): string { + const status = generateSpaceStatus(this.spaceApp.Status, this.spaceApp.App?.Status) + return `${this.name} (${status})` + } + + public buildDescription(): string { + return `${this.spaceApp.SpaceSharingSettingsSummary?.SharingType ?? 'Unknown'} space` + } + + public buildTooltip() { + const spaceName = this.spaceApp?.SpaceName ?? '-' + const appType = this.spaceApp?.SpaceSettingsSummary?.AppType || '-' + const domainId = this.spaceApp?.DomainId ?? '-' + const owner = this.spaceApp?.OwnershipSettingsSummary?.OwnerUserProfileName || '-' + const instanceType = this.spaceApp?.App?.ResourceSpec?.InstanceType ?? '-' + const remoteAccess = this.spaceApp?.SpaceSettingsSummary?.RemoteAccess + + let baseTooltip = '' + if (this.isSMUSSpace) { + baseTooltip = `**Space:** ${spaceName} \n\n**Application:** ${appType} \n\n**Instance Type:** ${instanceType}` + if (remoteAccess === RemoteAccess.ENABLED) { + baseTooltip += `\n\n**Remote Access:** Enabled` + } else if (remoteAccess === RemoteAccess.DISABLED) { + baseTooltip += `\n\n**Remote Access:** Disabled` + } + + return baseTooltip + } + return `**Space:** ${spaceName} \n\n**Application:** ${appType} \n\n**Domain ID:** ${domainId} \n\n**User Profile:** ${owner}` + } + + public getAppIcon() { + const appType = this.spaceApp.SpaceSettingsSummary?.AppType + if (appType === AppType.JupyterLab) { + return getIcon('aws-sagemaker-jupyter-lab') + } + if (appType === AppType.CodeEditor) { + return getIcon('aws-sagemaker-code-editor') + } + } + + public getContext(): string { + const status = this.getStatus() + + if (status === SpaceStatus.RUNNING) { + return 'awsSagemakerSpaceRunningNode' + } + + if (status === SpaceStatus.STOPPED) { + return 'awsSagemakerSpaceStoppedNode' + } + + // For all other states (STARTING, STOPPING, etc.), return base context + return this.isSMUSSpace ? 'smusSpaceNode' : 'awsSagemakerSpaceNode' + } + + public get DomainSpaceKey(): string { + return this.spaceApp.DomainSpaceKey! + } +} + +/** + * Sets up user activity monitoring for SageMaker spaces + */ +export async function setupUserActivityMonitoring(extensionContext: vscode.ExtensionContext): Promise { + logger.info('setupUserActivityMonitoring: Starting user activity monitoring setup') + + const tmpDirectory = '/tmp/' + const idleFilePath = path.join(tmpDirectory, '.sagemaker-last-active-timestamp') + logger.debug(`setupUserActivityMonitoring: Using idle file path: ${idleFilePath}`) + + try { + const userActivity = new UserActivity(ActivityCheckInterval) + userActivity.onUserActivity(() => { + logger.debug('setupUserActivityMonitoring: User activity detected, updating idle file') + void updateIdleFile(idleFilePath) + }) + + let terminalActivityInterval: NodeJS.Timeout | undefined = startMonitoringTerminalActivity(idleFilePath) + logger.debug('setupUserActivityMonitoring: Started terminal activity monitoring') + // Write initial timestamp + await updateIdleFile(idleFilePath) + logger.info('setupUserActivityMonitoring: Initial timestamp written successfully') + extensionContext.subscriptions.push(userActivity, { + dispose: () => { + logger.info('setupUserActivityMonitoring: Disposing user activity monitoring') + if (terminalActivityInterval) { + clearInterval(terminalActivityInterval) + terminalActivityInterval = undefined + } + }, + }) + + logger.info('setupUserActivityMonitoring: User activity monitoring setup completed successfully') + } catch (error) { + logger.error(`setupUserActivityMonitoring: Error during setup: ${error}`) + throw error + } +} diff --git a/packages/core/src/awsService/sagemaker/types.ts b/packages/core/src/awsService/sagemaker/types.ts index 9b06058ef62..76eb3c23ea9 100644 --- a/packages/core/src/awsService/sagemaker/types.ts +++ b/packages/core/src/awsService/sagemaker/types.ts @@ -6,11 +6,13 @@ export interface SpaceMappings { localCredential?: { [spaceName: string]: LocalCredentialProfile } deepLink?: { [spaceName: string]: DeeplinkSession } + smusProjects?: { [smusProjectId: string]: { accessKey: string; secret: string; token: string } } } export type LocalCredentialProfile = | { type: 'iam'; profileName: string } | { type: 'sso'; accessKey: string; secret: string; token: string } + | { type: 'sso' | 'iam'; smusProjectId: string } export interface DeeplinkSession { requests: Record diff --git a/packages/core/src/awsService/sagemaker/uriHandlers.ts b/packages/core/src/awsService/sagemaker/uriHandlers.ts index 17c3c512272..411fe5a789c 100644 --- a/packages/core/src/awsService/sagemaker/uriHandlers.ts +++ b/packages/core/src/awsService/sagemaker/uriHandlers.ts @@ -12,22 +12,50 @@ import { telemetry } from '../../shared/telemetry/telemetry' export function register(ctx: ExtContext) { async function connectHandler(params: ReturnType) { await telemetry.sagemaker_deeplinkConnect.run(async () => { + const wsUrl = `${params.ws_url}&cell-number=${params['cell-number']}` await deeplinkConnect( ctx, params.connection_identifier, params.session, - `${params.ws_url}&cell-number=${params['cell-number']}`, + wsUrl, params.token, - params.domain + params.domain, + params.app_type ) }) } - return vscode.Disposable.from(ctx.uriHandler.onPath('/connect/sagemaker', connectHandler, parseConnectParams)) + async function hyperPodConnectHandler(params: ReturnType) { + await telemetry.sagemaker_deeplinkConnect.run(async () => { + const wsUrl = `${params.streamUrl}&cell-number=${params['cell-number']}` + await deeplinkConnect( + ctx, + '', + params.sessionId, + wsUrl, + params.sessionToken, + '', + undefined, + params.workspaceName, + params.namespace, + params.eksClusterArn + ) + }) + } + + return vscode.Disposable.from( + ctx.uriHandler.onPath('/connect/sagemaker', connectHandler, parseConnectParams), + ctx.uriHandler.onPath('/connect/workspace', hyperPodConnectHandler, parseHyperpodConnectParams) + ) } +export function parseHyperpodConnectParams(query: SearchParams) { + const requiredParams = query.getFromKeysOrThrow('sessionId', 'streamUrl', 'sessionToken', 'cell-number') + const optionalParams = query.getFromKeys('workspaceName', 'namespace', 'eksClusterArn') + return { ...requiredParams, ...optionalParams } +} export function parseConnectParams(query: SearchParams) { - const params = query.getFromKeysOrThrow( + const requiredParams = query.getFromKeysOrThrow( 'connection_identifier', 'domain', 'user_profile', @@ -36,5 +64,7 @@ export function parseConnectParams(query: SearchParams) { 'cell-number', 'token' ) - return params + const optionalParams = query.getFromKeys('app_type') + + return { ...requiredParams, ...optionalParams } } diff --git a/packages/core/src/codecatalyst/utils.ts b/packages/core/src/codecatalyst/utils.ts index b28aea75d4d..3f9cf6fe0bf 100644 --- a/packages/core/src/codecatalyst/utils.ts +++ b/packages/core/src/codecatalyst/utils.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Ides } from 'aws-sdk/clients/codecatalyst' +import { Ide } from '@aws-sdk/client-codecatalyst' import * as vscode from 'vscode' import { CodeCatalystResource, getCodeCatalystConfig } from '../shared/clients/codecatalystClient' import { pushIf } from '../shared/utilities/collectionUtils' @@ -55,6 +55,6 @@ export function openCodeCatalystUrl(o: CodeCatalystResource) { } /** Returns true if the dev env has a "vscode" IDE runtime. */ -export function isDevenvVscode(ides: Ides | undefined): boolean { +export function isDevenvVscode(ides: Ide[] | undefined): boolean { return ides !== undefined && ides.some((ide) => ide.name === 'VSCode') } diff --git a/packages/core/src/codecatalyst/vue/create/backend.ts b/packages/core/src/codecatalyst/vue/create/backend.ts index bdf49419243..102c6329653 100644 --- a/packages/core/src/codecatalyst/vue/create/backend.ts +++ b/packages/core/src/codecatalyst/vue/create/backend.ts @@ -32,7 +32,7 @@ import { CancellationError } from '../../../shared/utilities/timeoutUtils' import { telemetry } from '../../../shared/telemetry/telemetry' import { isNonNullable } from '../../../shared/utilities/tsUtils' import { createOrgPrompter, createProjectPrompter } from '../../wizards/selectResource' -import { GetSourceRepositoryCloneUrlsRequest } from 'aws-sdk/clients/codecatalyst' +import { GetSourceRepositoryCloneUrlsRequest } from '@aws-sdk/client-codecatalyst' import { QuickPickPrompter } from '../../../shared/ui/pickerPrompter' interface LinkedResponse { diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index 941156a0d2e..e037657958d 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -157,21 +157,6 @@ export async function activate(context: ExtContext): Promise { } } - if (configurationChangeEvent.affectsConfiguration('amazonQ.shareContentWithAWS')) { - if (auth.isEnterpriseSsoInUse()) { - await vscode.window - .showInformationMessage( - CodeWhispererConstants.ssoConfigAlertMessageShareData, - CodeWhispererConstants.settingsLearnMore - ) - .then(async (resp) => { - if (resp === CodeWhispererConstants.settingsLearnMore) { - void openUrl(vscode.Uri.parse(CodeWhispererConstants.learnMoreUri)) - } - }) - } - } - if (configurationChangeEvent.affectsConfiguration('editor.inlineSuggest.enabled')) { await vscode.window .showInformationMessage( diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index 051254d1873..22ae0447d0a 100644 --- a/packages/core/src/codewhisperer/client/codewhisperer.ts +++ b/packages/core/src/codewhisperer/client/codewhisperer.ts @@ -227,6 +227,7 @@ export class DefaultCodeWhispererClient { product: 'CodeWhisperer', // TODO: update this? clientId: getClientId(globals.globalState), ideVersion: extensionVersion, + pluginVersion: extensionVersion, }, profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, } diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index 714937ed402..619ce74aa5b 100644 --- a/packages/core/src/codewhisperer/client/user-service-2.json +++ b/packages/core/src/codewhisperer/client/user-service-2.json @@ -29,6 +29,23 @@ "documentation": "

Creates a pre-signed, S3 write URL for uploading a repository zip archive.

", "idempotent": true }, + "CreateSubscriptionToken": { + "name": "CreateSubscriptionToken", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "CreateSubscriptionTokenRequest" }, + "output": { "shape": "CreateSubscriptionTokenResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ], + "idempotent": true + }, "CreateTaskAssistConversation": { "name": "CreateTaskAssistConversation", "http": { @@ -96,6 +113,7 @@ "errors": [ { "shape": "ThrottlingException" }, { "shape": "ConflictException" }, + { "shape": "ServiceQuotaExceededException" }, { "shape": "InternalServerException" }, { "shape": "ValidationException" }, { "shape": "AccessDeniedException" } @@ -270,6 +288,22 @@ ], "documentation": "

API to get code transformation status.

" }, + "GetUsageLimits": { + "name": "GetUsageLimits", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "GetUsageLimitsRequest" }, + "output": { "shape": "GetUsageLimitsResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ], + "documentation": "

API to get current usage limits

" + }, "ListAvailableCustomizations": { "name": "ListAvailableCustomizations", "http": { @@ -285,6 +319,21 @@ { "shape": "AccessDeniedException" } ] }, + "ListAvailableModels": { + "name": "ListAvailableModels", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "ListAvailableModelsRequest" }, + "output": { "shape": "ListAvailableModelsResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ] + }, "ListAvailableProfiles": { "name": "ListAvailableProfiles", "http": { @@ -382,6 +431,23 @@ ], "documentation": "

List workspace metadata based on a workspace root

" }, + "PushTelemetryEvent": { + "name": "PushTelemetryEvent", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "PushTelemetryEventRequest" }, + "output": { "shape": "PushTelemetryEventResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ], + "documentation": "

API to push telemetry events to CloudWatch, DataHub and EventBridge.

", + "idempotent": true + }, "ResumeTransformation": { "name": "ResumeTransformation", "http": { @@ -520,6 +586,23 @@ { "shape": "AccessDeniedException" } ], "documentation": "

API to stop code transformation status.

" + }, + "UpdateUsageLimits": { + "name": "UpdateUsageLimits", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "UpdateUsageLimitsRequest" }, + "output": { "shape": "UpdateUsageLimitsResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" }, + { "shape": "UpdateUsageLimitQuotaExceededException" } + ], + "documentation": "

API to update usage limits for enterprise customers

" } }, "shapes": { @@ -536,7 +619,17 @@ "AccessDeniedExceptionReason": { "type": "string", "documentation": "

Reason for AccessDeniedException

", - "enum": ["UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS"] + "enum": [ + "UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS", + "UNAUTHORIZED_WORKSPACE_CONTEXT_FEATURE_ACCESS", + "TEMPORARILY_SUSPENDED", + "FEATURE_NOT_SUPPORTED" + ] + }, + "ActivationToken": { + "type": "string", + "max": 11, + "min": 11 }, "ActiveFunctionalityList": { "type": "list", @@ -589,6 +682,15 @@ "max": 20, "min": 0 }, + "AgentTaskType": { + "type": "string", + "documentation": "

Type of agent task

", + "enum": ["vibe", "spectask"] + }, + "AgenticChatEventStatus": { + "type": "string", + "enum": ["SUCCEEDED", "CANCELLED", "FAILED"] + }, "AppStudioState": { "type": "structure", "required": ["namespace", "propertyName", "propertyContext"], @@ -691,13 +793,20 @@ "toolUses": { "shape": "ToolUses", "documentation": "

ToolUse Request

" + }, + "cachePoint": { + "shape": "CachePoint", + "documentation": "

Indicates whether this message is a cache point

" + }, + "reasoningContent": { + "shape": "ReasoningContent", + "documentation": "

Model's internal reasoning process, either as readable text or redacted binary content

" } }, "documentation": "

Markdown text message.

" }, "AssistantResponseMessageContentString": { "type": "string", - "max": 100000, "min": 0, "sensitive": true }, @@ -718,6 +827,7 @@ "min": 1, "pattern": "(?:[A-Za-z0-9\\+/]{4})*(?:[A-Za-z0-9\\+/]{2}\\=\\=|[A-Za-z0-9\\+/]{3}\\=)?" }, + "Blob": { "type": "blob" }, "Boolean": { "type": "boolean", "box": true @@ -730,6 +840,17 @@ "toggle": { "shape": "OptInFeatureToggle" } } }, + "CachePoint": { + "type": "structure", + "required": ["type"], + "members": { + "type": { "shape": "CachePointType" } + } + }, + "CachePointType": { + "type": "string", + "enum": ["default"] + }, "ChangeLogGranularityType": { "type": "string", "enum": ["STANDARD", "BUSINESS"] @@ -758,14 +879,14 @@ "requestLength": { "shape": "Integer" }, "responseLength": { "shape": "Integer" }, "numberOfCodeBlocks": { "shape": "Integer" }, - "hasProjectLevelContext": { "shape": "Boolean" } + "hasProjectLevelContext": { "shape": "Boolean" }, + "result": { "shape": "AgenticChatEventStatus" } } }, "ChatHistory": { "type": "list", "member": { "shape": "ChatMessage" }, "documentation": "

Indicates Participant in Chat conversation

", - "max": 250, "min": 0 }, "ChatInteractWithMessageEvent": { @@ -811,7 +932,8 @@ "CLICK_FOLLOW_UP", "HOVER_REFERENCE", "UPVOTE", - "DOWNVOTE" + "DOWNVOTE", + "AGENTIC_CODE_ACCEPTED" ] }, "ChatTriggerType": { @@ -831,6 +953,12 @@ "hasProjectLevelContext": { "shape": "Boolean" } } }, + "ClientCacheConfig": { + "type": "structure", + "members": { + "useClientCachingOnly": { "shape": "Boolean" } + } + }, "ClientId": { "type": "string", "max": 255, @@ -842,7 +970,7 @@ }, "CodeAnalysisScope": { "type": "string", - "enum": ["FILE", "PROJECT"] + "enum": ["FILE", "PROJECT", "AGENTIC"] }, "CodeAnalysisStatus": { "type": "string", @@ -868,9 +996,14 @@ "totalNewCodeCharacterCount": { "shape": "PrimitiveInteger" }, "totalNewCodeLineCount": { "shape": "PrimitiveInteger" }, "userWrittenCodeCharacterCount": { "shape": "CodeCoverageEventUserWrittenCodeCharacterCountInteger" }, - "userWrittenCodeLineCount": { "shape": "CodeCoverageEventUserWrittenCodeLineCountInteger" } + "userWrittenCodeLineCount": { "shape": "CodeCoverageEventUserWrittenCodeLineCountInteger" }, + "addedCharacterCount": { "shape": "CodeCoverageEventAddedCharacterCountInteger" } } }, + "CodeCoverageEventAddedCharacterCountInteger": { + "type": "integer", + "min": 0 + }, "CodeCoverageEventUserWrittenCodeCharacterCountInteger": { "type": "integer", "min": 0 @@ -1088,6 +1221,11 @@ "type": "string", "enum": ["SHA_256"] }, + "ContentType": { + "type": "string", + "documentation": "

The type of content

", + "enum": ["FILE", "PROMPT", "CODE", "WORKSPACE"] + }, "ContextTruncationScheme": { "type": "string", "documentation": "

Workspace context truncation schemes based on usecase

", @@ -1107,6 +1245,10 @@ "shape": "ConversationId", "documentation": "

Unique identifier for the chat conversation stream

" }, + "workspaceId": { + "shape": "UUID", + "documentation": "

Unique identifier for remote workspace

" + }, "history": { "shape": "ChatHistory", "documentation": "

Holds the history of chat messages.

" @@ -1119,10 +1261,34 @@ "shape": "ChatTriggerType", "documentation": "

Trigger Reason for Chat

" }, - "customizationArn": { "shape": "ResourceArn" } + "customizationArn": { "shape": "ResourceArn" }, + "agentContinuationId": { + "shape": "UUID", + "documentation": "

Unique identifier for the agent task execution

" + }, + "agentTaskType": { "shape": "AgentTaskType" } }, "documentation": "

Structure to represent the current state of a chat conversation.

" }, + "CreateSubscriptionTokenRequest": { + "type": "structure", + "members": { + "clientToken": { + "shape": "IdempotencyToken", + "idempotencyToken": true + }, + "statusOnly": { "shape": "Boolean" } + } + }, + "CreateSubscriptionTokenResponse": { + "type": "structure", + "required": ["status"], + "members": { + "encodedVerificationUrl": { "shape": "EncodedVerificationUrl" }, + "token": { "shape": "ActivationToken" }, + "status": { "shape": "SubscriptionStatus" } + } + }, "CreateTaskAssistConversationRequest": { "type": "structure", "members": { @@ -1204,7 +1370,7 @@ "CreateUserMemoryEntryInputProfileArnString": { "type": "string", "min": 1, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" + "pattern": "arn:aws:(codewhisperer|transform):[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" }, "CreateUserMemoryEntryOutput": { "type": "structure", @@ -1255,7 +1421,8 @@ "members": { "arn": { "shape": "CustomizationArn" }, "name": { "shape": "CustomizationName" }, - "description": { "shape": "Description" } + "description": { "shape": "Description" }, + "modelId": { "shape": "ModelId" } } }, "CustomizationArn": { @@ -1318,7 +1485,7 @@ "DeleteUserMemoryEntryInputProfileArnString": { "type": "string", "min": 1, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" + "pattern": "arn:aws:(codewhisperer|transform):[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" }, "DeleteUserMemoryEntryOutput": { "type": "structure", @@ -1547,6 +1714,11 @@ "type": "integer", "min": 0 }, + "Document": { + "type": "structure", + "members": {}, + "document": true + }, "DocumentSymbol": { "type": "structure", "required": ["name", "type"], @@ -1644,6 +1816,11 @@ }, "documentation": "

Represents the state of an Editor

" }, + "EncodedVerificationUrl": { + "type": "string", + "max": 8192, + "min": 1 + }, "EnvState": { "type": "structure", "members": { @@ -1931,7 +2108,8 @@ "optOutPreference": { "shape": "OptOutPreference" }, "userContext": { "shape": "UserContext" }, "profileArn": { "shape": "ProfileArn" }, - "workspaceId": { "shape": "UUID" } + "workspaceId": { "shape": "UUID" }, + "modelId": { "shape": "ModelId" } } }, "GenerateCompletionsRequestMaxResultsInteger": { @@ -1952,7 +2130,8 @@ "members": { "predictions": { "shape": "Predictions" }, "completions": { "shape": "Completions" }, - "nextToken": { "shape": "SensitiveString" } + "nextToken": { "shape": "SensitiveString" }, + "modelId": { "shape": "ModelId" } } }, "GetCodeAnalysisRequest": { @@ -2070,6 +2249,26 @@ }, "documentation": "

Structure to represent get code transformation response.

" }, + "GetUsageLimitsRequest": { + "type": "structure", + "members": { + "profileArn": { + "shape": "ProfileArn", + "documentation": "

The ARN of the Q Developer profile. Required for enterprise customers, optional for Builder ID users.

" + } + } + }, + "GetUsageLimitsResponse": { + "type": "structure", + "required": ["limits", "daysUntilReset"], + "members": { + "limits": { "shape": "UsageLimits" }, + "daysUntilReset": { + "shape": "Integer", + "documentation": "

Number of days remaining until the usage metrics reset

" + } + } + }, "GitState": { "type": "structure", "members": { @@ -2169,13 +2368,13 @@ "members": { "bytes": { "shape": "ImageSourceBytesBlob" } }, - "documentation": "

Image bytes limited to ~10MB considering overhead of base64 encoding

", + "documentation": "

Image bytes

", "sensitive": true, "union": true }, "ImageSourceBytesBlob": { "type": "blob", - "max": 1500000, + "max": 10000000, "min": 1 }, "Import": { @@ -2219,6 +2418,11 @@ "type": "string", "enum": ["ACCEPT", "REJECT", "DISMISS"] }, + "InputType": { + "type": "string", + "documentation": "

Types of input that can be processed by the model

", + "enum": ["IMAGE", "TEXT"] + }, "Integer": { "type": "integer", "box": true @@ -2238,13 +2442,19 @@ "type": "structure", "required": ["message"], "members": { - "message": { "shape": "String" } + "message": { "shape": "String" }, + "reason": { "shape": "InternalServerExceptionReason" } }, "documentation": "

This exception is thrown when an unexpected error occurred during the processing of a request.

", "exception": true, "fault": true, "retryable": { "throttling": false } }, + "InternalServerExceptionReason": { + "type": "string", + "documentation": "

Reason for InternalServerException

", + "enum": ["MODEL_TEMPORARILY_UNAVAILABLE"] + }, "IssuerUrl": { "type": "string", "max": 255, @@ -2276,6 +2486,52 @@ "nextToken": { "shape": "Base64EncodedPaginationToken" } } }, + "ListAvailableModelsRequest": { + "type": "structure", + "required": ["origin"], + "members": { + "origin": { + "shape": "Origin", + "documentation": "

The origin context for which to list available models

" + }, + "maxResults": { + "shape": "ListAvailableModelsRequestMaxResultsInteger", + "documentation": "

Maximum number of models to return in a single response

" + }, + "nextToken": { + "shape": "Base64EncodedPaginationToken", + "documentation": "

Token for retrieving the next page of results

" + }, + "profileArn": { + "shape": "ProfileArn", + "documentation": "

ARN of the profile to use for model filtering

" + }, + "modelProvider": { + "shape": "ModelProvider", + "documentation": "

Provider of AI models

" + } + } + }, + "ListAvailableModelsRequestMaxResultsInteger": { + "type": "integer", + "box": true, + "max": 100, + "min": 1 + }, + "ListAvailableModelsResponse": { + "type": "structure", + "required": ["models"], + "members": { + "models": { + "shape": "Models", + "documentation": "

List of available models

" + }, + "nextToken": { + "shape": "Base64EncodedPaginationToken", + "documentation": "

Token for retrieving the next page of results

" + } + } + }, "ListAvailableProfilesRequest": { "type": "structure", "members": { @@ -2379,12 +2635,12 @@ "ListUserMemoryEntriesInputNextTokenString": { "type": "string", "min": 1, - "pattern": "\\S+" + "pattern": "[A-Za-z0-9_-]+" }, "ListUserMemoryEntriesInputProfileArnString": { "type": "string", "min": 1, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" + "pattern": "arn:aws:(codewhisperer|transform):[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" }, "ListUserMemoryEntriesOutput": { "type": "structure", @@ -2397,7 +2653,7 @@ "ListUserMemoryEntriesOutputNextTokenString": { "type": "string", "min": 1, - "pattern": "\\S+" + "pattern": "[A-Za-z0-9_-]+" }, "ListWorkspaceMetadataRequest": { "type": "structure", @@ -2463,10 +2719,16 @@ "origin": { "shape": "Origin" }, "attributes": { "shape": "AttributesMap" }, "createdAt": { "shape": "Timestamp" }, - "updatedAt": { "shape": "Timestamp" } + "updatedAt": { "shape": "Timestamp" }, + "memoryStatus": { "shape": "MemoryStatus" } }, "documentation": "

Metadata for a single memory entry

" }, + "MemoryStatus": { + "type": "string", + "documentation": "

Status of user memory

", + "enum": ["DECRYPTION_FAILURE", "VALID"] + }, "MessageId": { "type": "string", "documentation": "

Unique identifier for the chat message

", @@ -2496,6 +2758,84 @@ "min": 1, "pattern": "[-a-zA-Z0-9._]*" }, + "Model": { + "type": "structure", + "required": ["modelId"], + "members": { + "modelId": { + "shape": "ModelId", + "documentation": "

Unique identifier for the model

" + }, + "modelName": { + "shape": "ModelName", + "documentation": "

User-facing display name

" + }, + "description": { + "shape": "ModelDescription", + "documentation": "

Description of the model

" + }, + "rateMultiplier": { + "shape": "ModelRateMultiplierDouble", + "documentation": "

Rate multiplier of the model

" + }, + "rateUnit": { + "shape": "ModelRateUnitString", + "documentation": "

Unit for the rate multiplier

" + }, + "tokenLimits": { + "shape": "TokenLimits", + "documentation": "

Limits on token usage for this model

" + }, + "supportedInputTypes": { + "shape": "SupportedInputTypesList", + "documentation": "

List of input types supported by this model

" + }, + "supportsPromptCache": { + "shape": "Boolean", + "documentation": "

Whether the model supports prompt caching

" + } + } + }, + "ModelDescription": { + "type": "string", + "max": 256, + "min": 0, + "pattern": "[\\sa-zA-Z0-9_.-]*" + }, + "ModelId": { + "type": "string", + "documentation": "

Unique identifier for the model

", + "max": 1024, + "min": 1, + "pattern": "[a-zA-Z0-9_:.-]+" + }, + "ModelName": { + "type": "string", + "documentation": "

Identifier for the model Name

", + "max": 1024, + "min": 1, + "pattern": "[a-zA-Z0-9-_. ]+" + }, + "ModelProvider": { + "type": "string", + "documentation": "

Provider of AI models

", + "enum": ["DEFAULT"] + }, + "ModelRateMultiplierDouble": { + "type": "double", + "box": true, + "max": 100.0, + "min": 0 + }, + "ModelRateUnitString": { + "type": "string", + "max": 100, + "min": 0 + }, + "Models": { + "type": "list", + "member": { "shape": "Model" } + }, "NextToken": { "type": "string", "max": 1000, @@ -2557,7 +2897,12 @@ "CLI", "AI_EDITOR", "OPENSEARCH_DASHBOARD", - "GITLAB" + "GITLAB", + "Q_DEV_BEXT", + "MD_IDE", + "MD_CE", + "SM_AI_STUDIO_IDE", + "INLINE_CHAT" ] }, "PackageInfo": { @@ -2630,7 +2975,7 @@ }, "PredictionType": { "type": "string", - "enum": ["Completions", "Edits"] + "enum": ["COMPLETIONS", "EDITS"] }, "PredictionTypes": { "type": "list", @@ -2674,7 +3019,7 @@ "type": "string", "max": 950, "min": 0, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" + "pattern": "arn:aws:(codewhisperer|transform):[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" }, "ProfileDescription": { "type": "string", @@ -2712,7 +3057,7 @@ "type": "string", "max": 128, "min": 1, - "pattern": "(python|javascript|java|csharp|typescript|c|cpp|go|kotlin|php|ruby|rust|scala|shell|sql|json|yaml|vue|tf|tsx|jsx|plaintext|systemverilog|dart|lua|swift|powershell|r)" + "pattern": "(python|javascript|java|csharp|typescript|c|cpp|go|kotlin|php|ruby|rust|scala|shell|sql|json|yaml|vue|tf|tsx|jsx|plaintext|systemverilog|dart|lua|swift|hcl|powershell|r|abap)" }, "ProgressUpdates": { "type": "list", @@ -2726,6 +3071,22 @@ "toggle": { "shape": "OptInFeatureToggle" } } }, + "PushTelemetryEventRequest": { + "type": "structure", + "required": ["eventType", "event"], + "members": { + "clientToken": { + "shape": "IdempotencyToken", + "idempotencyToken": true + }, + "eventType": { "shape": "String" }, + "event": { "shape": "Document" } + } + }, + "PushTelemetryEventResponse": { + "type": "structure", + "members": {} + }, "Range": { "type": "structure", "required": ["start", "end"], @@ -2741,6 +3102,31 @@ }, "documentation": "

Indicates Range / Span in a Text Document

" }, + "ReasoningContent": { + "type": "structure", + "members": { + "reasoningText": { "shape": "ReasoningText" }, + "redactedContent": { + "shape": "Blob", + "documentation": "

Reasoning content that was encrypted by the model provider

" + } + }, + "documentation": "

The entire reasoning content that the model used to return the output

", + "sensitive": true, + "union": true + }, + "ReasoningText": { + "type": "structure", + "required": ["text"], + "members": { + "text": { "shape": "SensitiveString" }, + "signature": { + "shape": "SensitiveString", + "documentation": "

A token that verifies that the reasoning text was generated by the model

" + } + }, + "sensitive": true + }, "RecommendationsWithReferencesPreference": { "type": "string", "documentation": "

Recommendations with references setting for CodeWhisperer

", @@ -2799,7 +3185,7 @@ "RelevantDocumentList": { "type": "list", "member": { "shape": "RelevantTextDocument" }, - "max": 30, + "max": 100, "min": 0 }, "RelevantTextDocument": { @@ -2821,6 +3207,10 @@ "documentSymbols": { "shape": "DocumentSymbols", "documentation": "

DocumentSymbols parsed from a text document

" + }, + "type": { + "shape": "ContentType", + "documentation": "

The type of content(file, prompt, symbol, or workspace)

" } }, "documentation": "

Represents an IDE retrieved relevant Text Document / File

" @@ -2962,7 +3352,8 @@ "telemetryEvent": { "shape": "TelemetryEvent" }, "optOutPreference": { "shape": "OptOutPreference" }, "userContext": { "shape": "UserContext" }, - "profileArn": { "shape": "ProfileArn" } + "profileArn": { "shape": "ProfileArn" }, + "modelId": { "shape": "ModelId" } } }, "SendTelemetryEventResponse": { @@ -2983,11 +3374,17 @@ "type": "structure", "required": ["message"], "members": { - "message": { "shape": "String" } + "message": { "shape": "String" }, + "reason": { "shape": "ServiceQuotaExceededExceptionReason" } }, "documentation": "

This exception is thrown when request was denied due to caller exceeding their usage limits

", "exception": true }, + "ServiceQuotaExceededExceptionReason": { + "type": "string", + "documentation": "

Reason for ServiceQuotaExceededException

", + "enum": ["CONVERSATION_LIMIT_EXCEEDED", "MONTHLY_REQUEST_COUNT", "OVERAGE_REQUEST_LIMIT_EXCEEDED"] + }, "ShellHistory": { "type": "list", "member": { "shape": "ShellHistoryEntry" }, @@ -3273,6 +3670,10 @@ "min": 1, "sensitive": true }, + "SubscriptionStatus": { + "type": "string", + "enum": ["INACTIVE", "ACTIVE"] + }, "SuggestedFix": { "type": "structure", "members": { @@ -3297,6 +3698,10 @@ "type": "string", "enum": ["ACCEPT", "REJECT", "DISCARD", "EMPTY", "MERGE"] }, + "SuggestionType": { + "type": "string", + "enum": ["COMPLETIONS", "EDITS"] + }, "SupplementalContext": { "type": "structure", "required": ["filePath", "content"], @@ -3322,7 +3727,7 @@ "SupplementalContextList": { "type": "list", "member": { "shape": "SupplementalContext" }, - "max": 5, + "max": 20, "min": 0 }, "SupplementalContextMetadata": { @@ -3369,7 +3774,7 @@ }, "SupplementaryWebLinkUrlString": { "type": "string", - "max": 1024, + "max": 2048, "min": 1, "sensitive": true }, @@ -3379,6 +3784,11 @@ "max": 10, "min": 0 }, + "SupportedInputTypesList": { + "type": "list", + "member": { "shape": "InputType" }, + "documentation": "

List of supported input types for the model

" + }, "SymbolType": { "type": "string", "enum": ["DECLARATION", "USAGE"] @@ -3742,13 +4152,37 @@ "ThrottlingExceptionReason": { "type": "string", "documentation": "

Reason for ThrottlingException

", - "enum": ["MONTHLY_REQUEST_COUNT"] + "enum": ["DAILY_REQUEST_COUNT", "MONTHLY_REQUEST_COUNT", "INSUFFICIENT_MODEL_CAPACITY"] }, "Timestamp": { "type": "timestamp" }, + "TokenLimits": { + "type": "structure", + "members": { + "maxInputTokens": { + "shape": "TokenLimitsMaxInputTokensInteger", + "documentation": "

Maximum number of input tokens the model can process

" + }, + "maxOutputTokens": { + "shape": "TokenLimitsMaxOutputTokensInteger", + "documentation": "

Maximum number of output tokens the model can produce

" + } + } + }, + "TokenLimitsMaxInputTokensInteger": { + "type": "integer", + "box": true, + "min": 1 + }, + "TokenLimitsMaxOutputTokensInteger": { + "type": "integer", + "box": true, + "min": 1 + }, "Tool": { "type": "structure", "members": { - "toolSpecification": { "shape": "ToolSpecification" } + "toolSpecification": { "shape": "ToolSpecification" }, + "cachePoint": { "shape": "CachePoint" } }, "documentation": "

Information about a tool that can be used.

", "union": true @@ -3772,7 +4206,7 @@ "documentation": "

The name for the tool.

", "max": 64, "min": 0, - "pattern": "[a-zA-Z][a-zA-Z0-9_]*", + "pattern": "[a-zA-Z0-9_-]+", "sensitive": true }, "ToolResult": { @@ -3808,7 +4242,7 @@ }, "ToolResultContentBlockTextString": { "type": "string", - "max": 800000, + "max": 10000000, "min": 0, "sensitive": true }, @@ -3819,9 +4253,7 @@ }, "ToolResults": { "type": "list", - "member": { "shape": "ToolResult" }, - "max": 10, - "min": 0 + "member": { "shape": "ToolResult" } }, "ToolSpecification": { "type": "structure", @@ -3855,9 +4287,7 @@ }, "ToolUses": { "type": "list", - "member": { "shape": "ToolUse" }, - "max": 10, - "min": 0 + "member": { "shape": "ToolUse" } }, "Tools": { "type": "list", @@ -4073,6 +4503,35 @@ "max": 36, "min": 36 }, + "UpdateUsageLimitQuotaExceededException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { "shape": "String" } + }, + "documentation": "

Exception thrown when the number of usage limit update requests exceeds the monthly quota (default 3 requests per month)

", + "exception": true + }, + "UpdateUsageLimitsRequest": { + "type": "structure", + "required": ["accountId", "featureType", "requestedLimit"], + "members": { + "accountId": { "shape": "String" }, + "accountlessUserId": { "shape": "String" }, + "featureType": { "shape": "UsageLimitType" }, + "requestedLimit": { "shape": "Long" }, + "justification": { "shape": "String" } + } + }, + "UpdateUsageLimitsResponse": { + "type": "structure", + "required": ["status"], + "members": { + "status": { "shape": "UsageLimitUpdateRequestStatus" }, + "approvedLimit": { "shape": "Long" }, + "remainingRequestsThisMonth": { "shape": "Integer" } + } + }, "UploadContext": { "type": "structure", "members": { @@ -4100,7 +4559,8 @@ "FULL_PROJECT_SECURITY_SCAN", "UNIT_TESTS_GENERATION", "CODE_FIX_GENERATION", - "WORKSPACE_CONTEXT" + "WORKSPACE_CONTEXT", + "AGENTIC_CODE_REVIEW" ] }, "Url": { @@ -4108,6 +4568,30 @@ "max": 1024, "min": 1 }, + "UsageLimitList": { + "type": "structure", + "required": ["type", "currentUsageLimit", "totalUsageLimit"], + "members": { + "type": { "shape": "UsageLimitType" }, + "currentUsageLimit": { "shape": "Long" }, + "totalUsageLimit": { "shape": "Long" }, + "percentUsed": { "shape": "Double" } + } + }, + "UsageLimitType": { + "type": "string", + "enum": ["CODE_COMPLETIONS", "AGENTIC_REQUEST", "AI_EDITOR", "TRANSFORM"] + }, + "UsageLimitUpdateRequestStatus": { + "type": "string", + "enum": ["APPROVED", "PENDING_REVIEW", "REJECTED"] + }, + "UsageLimits": { + "type": "list", + "member": { "shape": "UsageLimitList" }, + "max": 10, + "min": 0 + }, "UserContext": { "type": "structure", "required": ["ideCategory", "operatingSystem", "product"], @@ -4116,9 +4600,21 @@ "operatingSystem": { "shape": "OperatingSystem" }, "product": { "shape": "UserContextProductString" }, "clientId": { "shape": "UUID" }, - "ideVersion": { "shape": "String" } + "ideVersion": { "shape": "String" }, + "pluginVersion": { "shape": "UserContextPluginVersionString" }, + "lspVersion": { "shape": "UserContextLspVersionString" } } }, + "UserContextLspVersionString": { + "type": "string", + "max": 50, + "min": 0 + }, + "UserContextPluginVersionString": { + "type": "string", + "max": 50, + "min": 0 + }, "UserContextProductString": { "type": "string", "max": 128, @@ -4148,13 +4644,25 @@ "images": { "shape": "ImageBlocks", "documentation": "

Images associated with the Chat Message.

" + }, + "modelId": { + "shape": "ModelId", + "documentation": "

Unique identifier for the model used in this conversation

" + }, + "cachePoint": { + "shape": "CachePoint", + "documentation": "

Indicates whether to add a cache point after the current message

" + }, + "clientCacheConfig": { + "shape": "ClientCacheConfig", + "documentation": "

Client cache config

" } }, "documentation": "

Structure to represent a chat input message from User.

" }, "UserInputMessageContentString": { "type": "string", - "max": 600000, + "max": 10000000, "min": 0, "sensitive": true }, @@ -4243,9 +4751,21 @@ "customizationArn": { "shape": "CustomizationArn" }, "timestamp": { "shape": "Timestamp" }, "acceptedCharacterCount": { "shape": "PrimitiveInteger" }, - "unmodifiedAcceptedCharacterCount": { "shape": "PrimitiveInteger" } + "unmodifiedAcceptedCharacterCount": { "shape": "PrimitiveInteger" }, + "addedCharacterCount": { "shape": "UserModificationEventAddedCharacterCountInteger" }, + "unmodifiedAddedCharacterCount": { + "shape": "UserModificationEventUnmodifiedAddedCharacterCountInteger" + } } }, + "UserModificationEventAddedCharacterCountInteger": { + "type": "integer", + "min": 0 + }, + "UserModificationEventUnmodifiedAddedCharacterCountInteger": { + "type": "integer", + "min": 0 + }, "UserSettings": { "type": "structure", "members": { @@ -4280,9 +4800,25 @@ "perceivedLatencyMilliseconds": { "shape": "Double" }, "acceptedCharacterCount": { "shape": "PrimitiveInteger" }, "addedIdeDiagnostics": { "shape": "IdeDiagnosticList" }, - "removedIdeDiagnostics": { "shape": "IdeDiagnosticList" } + "removedIdeDiagnostics": { "shape": "IdeDiagnosticList" }, + "addedCharacterCount": { "shape": "UserTriggerDecisionEventAddedCharacterCountInteger" }, + "deletedCharacterCount": { "shape": "UserTriggerDecisionEventDeletedCharacterCountInteger" }, + "streakLength": { "shape": "UserTriggerDecisionEventStreakLengthInteger" }, + "suggestionType": { "shape": "SuggestionType" } } }, + "UserTriggerDecisionEventAddedCharacterCountInteger": { + "type": "integer", + "min": 0 + }, + "UserTriggerDecisionEventDeletedCharacterCountInteger": { + "type": "integer", + "min": 0 + }, + "UserTriggerDecisionEventStreakLengthInteger": { + "type": "integer", + "min": -1 + }, "ValidationException": { "type": "structure", "required": ["message"], @@ -4296,7 +4832,12 @@ "ValidationExceptionReason": { "type": "string", "documentation": "

Reason for ValidationException

", - "enum": ["INVALID_CONVERSATION_ID", "CONTENT_LENGTH_EXCEEDS_THRESHOLD", "INVALID_KMS_GRANT"] + "enum": [ + "INVALID_CONVERSATION_ID", + "CONTENT_LENGTH_EXCEEDS_THRESHOLD", + "INVALID_KMS_GRANT", + "INVALID_MODEL_ID" + ] }, "WorkspaceContext": { "type": "structure", diff --git a/packages/core/src/codewhisperer/commands/invokeRecommendation.ts b/packages/core/src/codewhisperer/commands/invokeRecommendation.ts new file mode 100644 index 00000000000..37fcb965774 --- /dev/null +++ b/packages/core/src/codewhisperer/commands/invokeRecommendation.ts @@ -0,0 +1,45 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { vsCodeState, ConfigurationEntry } from '../models/model' +import { resetIntelliSenseState } from '../util/globalStateUtil' +import { DefaultCodeWhispererClient } from '../client/codewhisperer' +import { RecommendationHandler } from '../service/recommendationHandler' +import { session } from '../util/codeWhispererSession' +import { RecommendationService } from '../service/recommendationService' + +/** + * This function is for manual trigger CodeWhisperer + */ + +export async function invokeRecommendation( + editor: vscode.TextEditor, + client: DefaultCodeWhispererClient, + config: ConfigurationEntry +) { + if (!editor || !config.isManualTriggerEnabled) { + return + } + + /** + * Skip when output channel gains focus and invoke + */ + if (editor.document.languageId === 'Log') { + return + } + /** + * When using intelliSense, if invocation position changed, reject previous active recommendations + */ + if (vsCodeState.isIntelliSenseActive && editor.selection.active !== session.startPos) { + resetIntelliSenseState( + config.isManualTriggerEnabled, + config.isAutomatedTriggerEnabled, + RecommendationHandler.instance.isValidResponse() + ) + } + + await RecommendationService.instance.generateRecommendation(client, editor, 'OnDemand', config, undefined) +} diff --git a/packages/core/src/codewhisperer/commands/onAcceptance.ts b/packages/core/src/codewhisperer/commands/onAcceptance.ts new file mode 100644 index 00000000000..e13c197cefd --- /dev/null +++ b/packages/core/src/codewhisperer/commands/onAcceptance.ts @@ -0,0 +1,85 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { vsCodeState, OnRecommendationAcceptanceEntry } from '../models/model' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { CodeWhispererTracker } from '../tracker/codewhispererTracker' +import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' +import { getLogger } from '../../shared/logger/logger' +import { handleExtraBrackets } from '../util/closingBracketUtil' +import { RecommendationHandler } from '../service/recommendationHandler' +import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' +import { ReferenceHoverProvider } from '../service/referenceHoverProvider' +import path from 'path' + +/** + * This function is called when user accepts a intelliSense suggestion or an inline suggestion + */ +export async function onAcceptance(acceptanceEntry: OnRecommendationAcceptanceEntry) { + RecommendationHandler.instance.cancelPaginatedRequest() + /** + * Format document + */ + if (acceptanceEntry.editor) { + const languageContext = runtimeLanguageContext.getLanguageContext( + acceptanceEntry.editor.document.languageId, + path.extname(acceptanceEntry.editor.document.fileName) + ) + const start = acceptanceEntry.range.start + const end = acceptanceEntry.range.end + + // codewhisperer will be doing editing while formatting. + // formatting should not trigger consoals auto trigger + vsCodeState.isCodeWhispererEditing = true + /** + * Mitigation to right context handling mainly for auto closing bracket use case + */ + try { + await handleExtraBrackets(acceptanceEntry.editor, end, start) + } catch (error) { + getLogger().error(`${error} in handleAutoClosingBrackets`) + } + // move cursor to end of suggestion before doing code format + // after formatting, the end position will still be editor.selection.active + acceptanceEntry.editor.selection = new vscode.Selection(end, end) + + vsCodeState.isCodeWhispererEditing = false + CodeWhispererTracker.getTracker().enqueue({ + time: new Date(), + fileUrl: acceptanceEntry.editor.document.uri, + originalString: acceptanceEntry.editor.document.getText(new vscode.Range(start, end)), + startPosition: start, + endPosition: end, + requestId: acceptanceEntry.requestId, + sessionId: acceptanceEntry.sessionId, + index: acceptanceEntry.acceptIndex, + triggerType: acceptanceEntry.triggerType, + completionType: acceptanceEntry.completionType, + language: languageContext.language, + }) + const insertedCoderange = new vscode.Range(start, end) + CodeWhispererCodeCoverageTracker.getTracker(languageContext.language)?.countAcceptedTokens( + insertedCoderange, + acceptanceEntry.editor.document.getText(insertedCoderange), + acceptanceEntry.editor.document.fileName + ) + if (acceptanceEntry.references !== undefined) { + const referenceLog = ReferenceLogViewProvider.getReferenceLog( + acceptanceEntry.recommendation, + acceptanceEntry.references, + acceptanceEntry.editor + ) + ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) + ReferenceHoverProvider.instance.addCodeReferences( + acceptanceEntry.recommendation, + acceptanceEntry.references + ) + } + } + + // at the end of recommendation acceptance, report user decisions and clear recommendations. + RecommendationHandler.instance.reportUserDecisions(acceptanceEntry.acceptIndex) +} diff --git a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts new file mode 100644 index 00000000000..d193af056f7 --- /dev/null +++ b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts @@ -0,0 +1,145 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as CodeWhispererConstants from '../models/constants' +import { vsCodeState, OnRecommendationAcceptanceEntry } from '../models/model' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { CodeWhispererTracker } from '../tracker/codewhispererTracker' +import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' +import { getLogger } from '../../shared/logger/logger' +import { RecommendationHandler } from '../service/recommendationHandler' +import { sleep } from '../../shared/utilities/timeoutUtils' +import { handleExtraBrackets } from '../util/closingBracketUtil' +import { Commands } from '../../shared/vscode/commands2' +import { isInlineCompletionEnabled } from '../util/commonUtil' +import { onAcceptance } from './onAcceptance' +import * as codewhispererClient from '../client/codewhisperer' +import { + CodewhispererCompletionType, + CodewhispererLanguage, + CodewhispererTriggerType, +} from '../../shared/telemetry/telemetry.gen' +import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' +import { ReferenceHoverProvider } from '../service/referenceHoverProvider' +import { ImportAdderProvider } from '../service/importAdderProvider' +import { session } from '../util/codeWhispererSession' +import path from 'path' +import { RecommendationService } from '../service/recommendationService' +import { Container } from '../service/serviceContainer' +import { telemetry } from '../../shared/telemetry/telemetry' +import { TelemetryHelper } from '../util/telemetryHelper' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' + +export const acceptSuggestion = Commands.declare( + 'aws.amazonq.accept', + (context: vscode.ExtensionContext) => + async ( + range: vscode.Range, + effectiveRange: vscode.Range, + acceptIndex: number, + recommendation: string, + requestId: string, + sessionId: string, + triggerType: CodewhispererTriggerType, + completionType: CodewhispererCompletionType, + language: CodewhispererLanguage, + references: codewhispererClient.References + ) => { + telemetry.record({ + traceId: TelemetryHelper.instance.traceId, + }) + + RecommendationService.instance.incrementAcceptedCount() + const editor = vscode.window.activeTextEditor + await Container.instance.lineAnnotationController.refresh(editor, 'codewhisperer') + const onAcceptanceFunc = isInlineCompletionEnabled() ? onInlineAcceptance : onAcceptance + await onAcceptanceFunc({ + editor, + range, + effectiveRange, + acceptIndex, + recommendation, + requestId, + sessionId, + triggerType, + completionType, + language, + references, + }) + } +) +/** + * This function is called when user accepts a intelliSense suggestion or an inline suggestion + */ +export async function onInlineAcceptance(acceptanceEntry: OnRecommendationAcceptanceEntry) { + RecommendationHandler.instance.cancelPaginatedRequest() + RecommendationHandler.instance.disposeInlineCompletion() + + if (acceptanceEntry.editor) { + await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) + const languageContext = runtimeLanguageContext.getLanguageContext( + acceptanceEntry.editor.document.languageId, + path.extname(acceptanceEntry.editor.document.fileName) + ) + const start = acceptanceEntry.range.start + const end = acceptanceEntry.editor.selection.active + + vsCodeState.isCodeWhispererEditing = true + /** + * Mitigation to right context handling mainly for auto closing bracket use case + */ + try { + // Do not handle extra bracket if there is a right context merge + if (acceptanceEntry.recommendation === session.recommendations[acceptanceEntry.acceptIndex].content) { + await handleExtraBrackets(acceptanceEntry.editor, end, acceptanceEntry.effectiveRange.start) + } + await ImportAdderProvider.instance.onAcceptRecommendation( + acceptanceEntry.editor, + session.recommendations[acceptanceEntry.acceptIndex], + start.line + ) + } catch (error) { + getLogger().error(`${error} in handling extra brackets or imports`) + } finally { + vsCodeState.isCodeWhispererEditing = false + } + + CodeWhispererTracker.getTracker().enqueue({ + time: new Date(), + fileUrl: acceptanceEntry.editor.document.uri, + originalString: acceptanceEntry.editor.document.getText(new vscode.Range(start, end)), + startPosition: start, + endPosition: end, + requestId: acceptanceEntry.requestId, + sessionId: acceptanceEntry.sessionId, + index: acceptanceEntry.acceptIndex, + triggerType: acceptanceEntry.triggerType, + completionType: acceptanceEntry.completionType, + language: languageContext.language, + }) + const insertedCoderange = new vscode.Range(start, end) + CodeWhispererCodeCoverageTracker.getTracker(languageContext.language)?.countAcceptedTokens( + insertedCoderange, + acceptanceEntry.editor.document.getText(insertedCoderange), + acceptanceEntry.editor.document.fileName + ) + UserWrittenCodeTracker.instance.onQFinishesEdits() + if (acceptanceEntry.references !== undefined) { + const referenceLog = ReferenceLogViewProvider.getReferenceLog( + acceptanceEntry.recommendation, + acceptanceEntry.references, + acceptanceEntry.editor + ) + ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) + ReferenceHoverProvider.instance.addCodeReferences( + acceptanceEntry.recommendation, + acceptanceEntry.references + ) + } + + RecommendationHandler.instance.reportUserDecisions(acceptanceEntry.acceptIndex) + } +} diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index bd081face38..ba9f4f9d926 100644 --- a/packages/core/src/codewhisperer/commands/startSecurityScan.ts +++ b/packages/core/src/codewhisperer/commands/startSecurityScan.ts @@ -482,7 +482,10 @@ export function errorPromptHelper( }) } if (error.code !== 'NoSourceFilesError') { - void vscode.window.showWarningMessage(getErrorMessage(error), ok) + // Skip showing warning messages during tests to avoid interfering with test dialogs + if (process.env.NODE_ENV !== 'test') { + void vscode.window.showWarningMessage(getErrorMessage(error), ok) + } } } diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index aa8bea11da2..410465a55c1 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -769,7 +769,12 @@ export async function postTransformationJob() { latest.status, latest.duration, transformByQState.getJobId(), - transformByQState.getJobHistoryPath() + transformByQState.getJobHistoryPath(), + latest.transformationType, + latest.sourceJDKVersion, + latest.targetJDKVersion, + latest.customDependencyVersionsFilePath, + latest.customBuildCommand ) } } diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index ac43fba46aa..066e5ca2fcb 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -36,6 +36,7 @@ export { codeWhispererClient, } from './client/codewhisperer' export { listCodeWhispererCommands, listCodeWhispererCommandsId } from './ui/statusBarMenu' +export { InlineCompletionService } from './service/inlineCompletionService' export { refreshStatusBar, CodeWhispererStatusBarManager } from './service/statusBar' export { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider' export { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider' @@ -46,30 +47,44 @@ export { IssueItem, SeverityItem, } from './service/securityIssueTreeViewProvider' +export { onAcceptance } from './commands/onAcceptance' export { CodeWhispererTracker } from './tracker/codewhispererTracker' export { CodeWhispererUserGroupSettings } from './util/userGroupUtil' export { session } from './util/codeWhispererSession' +export { onInlineAcceptance } from './commands/onInlineAcceptance' export { stopTransformByQ } from './commands/startTransformByQ' export { featureDefinitions, FeatureConfigProvider } from '../shared/featureConfig' export { ReferenceInlineProvider } from './service/referenceInlineProvider' export { ReferenceHoverProvider } from './service/referenceHoverProvider' +export { CWInlineCompletionItemProvider } from './service/inlineCompletionItemProvider' +export { ClassifierTrigger } from './service/classifierTrigger' export { ReferenceLogViewProvider } from './service/referenceLogViewProvider' +export { RecommendationService } from './service/recommendationService' export { ImportAdderProvider } from './service/importAdderProvider' export { LicenseUtil } from './util/licenseUtil' export { SecurityIssueProvider } from './service/securityIssueProvider' export { listScanResults, mapToAggregatedList, pollScanJobStatus } from './service/securityScanHandler' export { TelemetryHelper } from './util/telemetryHelper' export { LineSelection, LineTracker } from './tracker/lineTracker' +export { BM25Okapi } from './util/supplementalContext/rankBm25' export { runtimeLanguageContext, RuntimeLanguageContext } from './util/runtimeLanguageContext' export * as startSecurityScan from './commands/startSecurityScan' +export * from './util/supplementalContext/utgUtils' +export * from './util/supplementalContext/crossFileContextUtil' +export * from './util/editorContext' +export { acceptSuggestion } from './commands/onInlineAcceptance' export * from './util/showSsoPrompt' export * from './util/securityScanLanguageContext' export * from './util/importAdderUtil' +export * from './util/globalStateUtil' export * from './util/zipUtil' export * from './util/diagnosticsUtil' export * from './util/commonUtil' export * from './util/closingBracketUtil' +export * from './util/supplementalContext/codeParsingUtil' +export * from './util/supplementalContext/supplementalContextUtil' export * from './util/codewhispererSettings' +export * as supplementalContextUtil from './util/supplementalContext/supplementalContextUtil' export * from './service/diagnosticsProvider' export * as diagnosticsProvider from './service/diagnosticsProvider' export * from './ui/codeWhispererNodes' @@ -87,3 +102,7 @@ export * from './util/gitUtil' export * from './ui/prompters' export { UserWrittenCodeTracker } from './tracker/userWrittenCodeTracker' export { RegionProfileManager, defaultServiceConfig } from './region/regionProfileManager' +export { DocumentChangedSource, KeyStrokeHandler, DefaultDocumentChangedType } from './service/keyStrokeHandler' +export { RecommendationHandler } from './service/recommendationHandler' +export { CodeWhispererCodeCoverageTracker } from './tracker/codewhispererCodeCoverageTracker' +export { invokeRecommendation } from './commands/invokeRecommendation' diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index f3bbfb07d85..2dc53f0fdd8 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -138,10 +138,16 @@ export const runningSecurityScan = 'Reviewing project for code issues...' export const runningFileScan = 'Reviewing current file for code issues...' +export const noSuggestions = 'No suggestions from Amazon Q' + export const noInlineSuggestionsMsg = 'No suggestions from Amazon Q' export const licenseFilter = 'Amazon Q suggestions were filtered due to reference settings' +/** + * the interval of the background thread invocation, which is triggered by the timer + */ +export const defaultCheckPeriodMillis = 1000 * 60 * 5 /** * Key bindings JSON file path */ @@ -582,8 +588,8 @@ export const invalidMetadataFileUnsupportedSourceDB = export const invalidMetadataFileUnsupportedTargetDB = 'I can only convert SQL for migrations to Aurora PostgreSQL or Amazon RDS for PostgreSQL target databases. The provided .sct file indicates another target database for this migration.' -export const invalidCustomVersionsFileMessage = (missingKey: string) => - `The dependency upgrade file provided is missing required field \`${missingKey}\`. Check that it is configured properly and try again. For an example of the required dependency upgrade file format, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file).` +export const invalidCustomVersionsFileMessage = (errorMessage: string) => + `The dependency upgrade file provided is malformed: ${errorMessage}. Check that it is configured properly and try again. For an example of the required dependency upgrade file format, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file).` export const invalidMetadataFileErrorParsing = "It looks like the .sct file you provided isn't valid. Make sure that you've uploaded the .zip file you retrieved from your schema conversion in AWS DMS." @@ -936,6 +942,10 @@ export const predictionTrackerDefaultConfig = { maxSupplementalContext: 15, } +export enum InlineCompletionLoggingReason { + IMPLICIT_REJECT = 'IMPLICIT_REJECT', +} + export const codeReviewFindingsSuffix = '_codeReviewFindings' export const displayFindingsSuffix = '_displayFindings' diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index bcfa50c6a71..f074fe74bd6 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -688,7 +688,17 @@ export const jobPlanProgress: { } export let sessionJobHistory: { - [jobId: string]: { startTime: string; projectName: string; status: string; duration: string } + [jobId: string]: { + startTime: string + projectName: string + status: string + duration: string + transformationType: string + sourceJDKVersion: string + targetJDKVersion: string + customDependencyVersionsFilePath: string + customBuildCommand: string + } } = {} export class TransformByQState { diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts index 24d58d7f588..aab2ed04dab 100644 --- a/packages/core/src/codewhisperer/region/regionProfileManager.ts +++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts @@ -18,7 +18,8 @@ import { import globals from '../../shared/extensionGlobals' import { once } from '../../shared/utilities/functionUtils' import CodeWhispererUserClient from '../client/codewhispereruserclient' -import { Credentials, Service } from 'aws-sdk' +import { AwsCredentialIdentity } from '@aws-sdk/types' +import { Service } from 'aws-sdk' import { ServiceOptions } from '../../shared/awsClientBuilder' import userApiConfig = require('../client/user-service-2.json') import { createConstantMap } from '../../shared/utilities/tsUtils' @@ -394,7 +395,7 @@ export class RegionProfileManager { apiConfig: userApiConfig, region: region, endpoint: endpoint, - credentials: new Credentials({ accessKeyId: 'xxx', secretAccessKey: 'xxx' }), + credentials: { accessKeyId: 'xxx', secretAccessKey: 'xxx' } as AwsCredentialIdentity, onRequestSetup: [ (req) => { req.on('build', ({ httpRequest }) => { diff --git a/packages/core/src/codewhisperer/service/classifierTrigger.ts b/packages/core/src/codewhisperer/service/classifierTrigger.ts new file mode 100644 index 00000000000..842d5312e68 --- /dev/null +++ b/packages/core/src/codewhisperer/service/classifierTrigger.ts @@ -0,0 +1,609 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import os from 'os' +import * as vscode from 'vscode' +import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' +import { extractContextForCodeWhisperer } from '../util/editorContext' +import { TelemetryHelper } from '../util/telemetryHelper' +import { ProgrammingLanguage } from '../client/codewhispereruserclient' + +interface normalizedCoefficients { + readonly lineNum: number + readonly lenLeftCur: number + readonly lenLeftPrev: number + readonly lenRight: number +} +/* + uses ML classifier to determine if user input should trigger CWSPR service + */ +export class ClassifierTrigger { + static #instance: ClassifierTrigger + + public static get instance() { + return (this.#instance ??= new this()) + } + + // ML classifier trigger threshold + private triggerThreshold = 0.43 + + // ML classifier coefficients + // os coefficient + private osCoefficientMap: Readonly> = { + 'Mac OS X': -0.1552, + 'Windows 10': -0.0238, + Windows: 0.0412, + win32: -0.0559, + } + + // trigger type coefficient + private triggerTypeCoefficientMap: Readonly> = { + SpecialCharacters: 0.0209, + Enter: 0.2853, + } + + private languageCoefficientMap: Readonly> = { + java: -0.4622, + javascript: -0.4688, + python: -0.3052, + typescript: -0.6084, + tsx: -0.6084, + jsx: -0.4688, + shell: -0.4718, + ruby: -0.7356, + sql: -0.4937, + rust: -0.4309, + kotlin: -0.4739, + php: -0.3917, + csharp: -0.3475, + go: -0.3504, + scala: -0.534, + cpp: -0.1734, + json: 0, + yaml: -0.3, + tf: -0.55, + } + + // other metadata coefficient + private lineNumCoefficient = -0.0416 + private lengthOfLeftCurrentCoefficient = -1.1747 + private lengthOfLeftPrevCoefficient = 0.4033 + private lengthOfRightCoefficient = -0.3321 + private prevDecisionAcceptCoefficient = 0.5397 + private prevDecisionRejectCoefficient = -0.1656 + private prevDecisionOtherCoefficient = 0 + private ideVscode = -0.1905 + private lengthLeft0To5 = -0.8756 + private lengthLeft5To10 = -0.5463 + private lengthLeft10To20 = -0.4081 + private lengthLeft20To30 = -0.3272 + private lengthLeft30To40 = -0.2442 + private lengthLeft40To50 = -0.1471 + + // intercept of logistic regression classifier + private intercept = 0.3738713 + + private maxx: normalizedCoefficients = { + lineNum: 4631.0, + lenLeftCur: 157.0, + lenLeftPrev: 176.0, + lenRight: 10239.0, + } + + private minn: normalizedCoefficients = { + lineNum: 0.0, + lenLeftCur: 0.0, + lenLeftPrev: 0.0, + lenRight: 0.0, + } + + // character and keywords coefficient + private charCoefficient: Readonly> = { + throw: 1.5868, + ';': -1.268, + any: -1.1565, + '7': -1.1347, + false: -1.1307, + nil: -1.0653, + elif: 1.0122, + '9': -1.0098, + pass: -1.0058, + True: -1.0002, + False: -0.9434, + '6': -0.9222, + true: -0.9142, + None: -0.9027, + '8': -0.9013, + break: -0.8475, + '}': -0.847, + '5': -0.8414, + '4': -0.8197, + '1': -0.8085, + '\\': -0.8019, + static: -0.7748, + '0': -0.77, + end: -0.7617, + '(': 0.7239, + '/': -0.7104, + where: -0.6981, + readonly: -0.6741, + async: -0.6723, + '3': -0.654, + continue: -0.6413, + struct: -0.64, + try: -0.6369, + float: -0.6341, + using: 0.6079, + '@': 0.6016, + '|': 0.5993, + impl: 0.5808, + private: -0.5746, + for: 0.5741, + '2': -0.5634, + let: -0.5187, + foreach: 0.5186, + select: -0.5148, + export: -0.5, + mut: -0.4921, + ')': -0.463, + ']': -0.4611, + when: 0.4602, + virtual: -0.4583, + extern: -0.4465, + catch: 0.4446, + new: 0.4394, + val: -0.4339, + map: 0.4284, + case: 0.4271, + throws: 0.4221, + null: -0.4197, + protected: -0.4133, + q: 0.4125, + except: 0.4115, + ': ': 0.4072, + '^': -0.407, + ' ': 0.4066, + $: 0.3981, + this: 0.3962, + switch: 0.3947, + '*': -0.3931, + module: 0.3912, + array: 0.385, + '=': 0.3828, + p: 0.3728, + ON: 0.3708, + '`': 0.3693, + u: 0.3658, + a: 0.3654, + require: 0.3646, + '>': -0.3644, + const: -0.3476, + o: 0.3423, + sizeof: 0.3416, + object: 0.3362, + w: 0.3345, + print: 0.3344, + range: 0.3336, + if: 0.3324, + abstract: -0.3293, + var: -0.3239, + i: 0.321, + while: 0.3138, + J: 0.3137, + c: 0.3118, + await: -0.3072, + from: 0.3057, + f: 0.302, + echo: 0.2995, + '#': 0.2984, + e: 0.2962, + r: 0.2925, + mod: 0.2893, + loop: 0.2874, + t: 0.2832, + '~': 0.282, + final: -0.2816, + del: 0.2785, + override: -0.2746, + ref: -0.2737, + h: 0.2693, + m: 0.2681, + '{': 0.2674, + implements: 0.2672, + inline: -0.2642, + match: 0.2613, + with: -0.261, + x: 0.2597, + namespace: -0.2596, + operator: 0.2573, + double: -0.2563, + source: -0.2482, + import: -0.2419, + NULL: -0.2399, + l: 0.239, + or: 0.2378, + s: 0.2366, + then: 0.2354, + W: 0.2354, + y: 0.2333, + local: 0.2288, + is: 0.2282, + n: 0.2254, + '+': -0.2251, + G: 0.223, + public: -0.2229, + WHERE: 0.2224, + list: 0.2204, + Q: 0.2204, + '[': 0.2136, + VALUES: 0.2134, + H: 0.2105, + g: 0.2094, + else: -0.208, + bool: -0.2066, + long: -0.2059, + R: 0.2025, + S: 0.2021, + d: 0.2003, + V: 0.1974, + K: -0.1961, + '<': 0.1958, + debugger: -0.1929, + NOT: -0.1911, + b: 0.1907, + boolean: -0.1891, + z: -0.1866, + LIKE: -0.1793, + raise: 0.1782, + L: 0.1768, + fn: 0.176, + delete: 0.1714, + unsigned: -0.1675, + auto: -0.1648, + finally: 0.1616, + k: 0.1599, + as: 0.156, + instanceof: 0.1558, + '&': 0.1554, + E: 0.1551, + M: 0.1542, + I: 0.1503, + Y: 0.1493, + typeof: 0.1475, + j: 0.1445, + INTO: 0.1442, + IF: 0.1437, + next: 0.1433, + undef: -0.1427, + THEN: -0.1416, + v: 0.1415, + C: 0.1383, + P: 0.1353, + AND: -0.1345, + constructor: 0.1337, + void: -0.1336, + class: -0.1328, + defer: 0.1316, + begin: 0.1306, + FROM: -0.1304, + SET: 0.1291, + decimal: -0.1278, + friend: 0.1277, + SELECT: -0.1265, + event: 0.1259, + lambda: 0.1253, + enum: 0.1215, + A: 0.121, + lock: 0.1187, + ensure: 0.1184, + '%': 0.1177, + isset: 0.1175, + O: 0.1174, + '.': 0.1146, + UNION: -0.1145, + alias: -0.1129, + template: -0.1102, + WHEN: 0.1093, + rescue: 0.1083, + DISTINCT: -0.1074, + trait: -0.1073, + D: 0.1062, + in: 0.1045, + internal: -0.1029, + ',': 0.1027, + static_cast: 0.1016, + do: -0.1005, + OR: 0.1003, + AS: -0.1001, + interface: 0.0996, + super: 0.0989, + B: 0.0963, + U: 0.0962, + T: 0.0943, + CALL: -0.0918, + BETWEEN: -0.0915, + N: 0.0897, + yield: 0.0867, + done: -0.0857, + string: -0.0837, + out: -0.0831, + volatile: -0.0819, + retry: 0.0816, + '?': -0.0796, + number: -0.0791, + short: 0.0787, + sealed: -0.0776, + package: 0.0765, + OPEN: -0.0756, + base: 0.0735, + and: 0.0729, + exit: 0.0726, + _: 0.0721, + keyof: -0.072, + def: 0.0713, + crate: -0.0706, + '-': -0.07, + FUNCTION: 0.0692, + declare: -0.0678, + include: 0.0671, + COUNT: -0.0669, + INDEX: -0.0666, + CLOSE: -0.0651, + fi: -0.0644, + uint: 0.0624, + params: 0.0575, + HAVING: 0.0575, + byte: -0.0575, + clone: -0.0552, + char: -0.054, + func: 0.0538, + never: -0.053, + unset: -0.0524, + unless: -0.051, + esac: -0.0509, + shift: -0.0507, + require_once: 0.0486, + ELSE: -0.0477, + extends: 0.0461, + elseif: 0.0452, + mutable: -0.0451, + asm: 0.0449, + '!': 0.0446, + LIMIT: 0.0444, + ushort: -0.0438, + '"': -0.0433, + Z: 0.0431, + exec: -0.0431, + IS: -0.0429, + DECLARE: -0.0425, + __LINE__: -0.0424, + BEGIN: -0.0418, + typedef: 0.0414, + EXIT: -0.0412, + "'": 0.041, + function: -0.0393, + dyn: -0.039, + wchar_t: -0.0388, + unique: -0.0383, + include_once: 0.0367, + stackalloc: 0.0359, + RETURN: -0.0356, + const_cast: 0.035, + MAX: 0.0341, + assert: -0.0331, + JOIN: -0.0328, + use: 0.0318, + GET: 0.0317, + VIEW: 0.0314, + move: 0.0308, + typename: 0.0308, + die: 0.0305, + asserts: -0.0304, + reinterpret_cast: -0.0302, + USING: -0.0289, + elsif: -0.0285, + FIRST: -0.028, + self: -0.0278, + RETURNING: -0.0278, + symbol: -0.0273, + OFFSET: 0.0263, + bigint: 0.0253, + register: -0.0237, + union: -0.0227, + return: -0.0227, + until: -0.0224, + endfor: -0.0213, + implicit: -0.021, + LOOP: 0.0195, + pub: 0.0182, + global: 0.0179, + EXCEPTION: 0.0175, + delegate: 0.0173, + signed: -0.0163, + FOR: 0.0156, + unsafe: 0.014, + NEXT: -0.0133, + IN: 0.0129, + MIN: -0.0123, + go: -0.0112, + type: -0.0109, + explicit: -0.0107, + eval: -0.0104, + int: -0.0099, + CASE: -0.0096, + END: 0.0084, + UPDATE: 0.0074, + default: 0.0072, + chan: 0.0068, + fixed: 0.0066, + not: -0.0052, + X: -0.0047, + endforeach: 0.0031, + goto: 0.0028, + empty: 0.0022, + checked: 0.0012, + F: -0.001, + } + + public getThreshold() { + return this.triggerThreshold + } + + public recordClassifierResultForManualTrigger(editor: vscode.TextEditor) { + this.shouldTriggerFromClassifier(undefined, editor, undefined, true) + } + + public recordClassifierResultForAutoTrigger( + editor: vscode.TextEditor, + triggerType?: CodewhispererAutomatedTriggerType, + event?: vscode.TextDocumentChangeEvent + ) { + if (!triggerType) { + return + } + this.shouldTriggerFromClassifier(event, editor, triggerType, true) + } + + public shouldTriggerFromClassifier( + event: vscode.TextDocumentChangeEvent | undefined, + editor: vscode.TextEditor, + autoTriggerType: string | undefined, + shouldRecordResult: boolean = false + ): boolean { + const fileContext = extractContextForCodeWhisperer(editor) + const osPlatform = this.normalizeOsName(os.platform(), os.version()) + const char = event ? event.contentChanges[0].text : '' + const lineNum = editor.selection.active.line + const classifierResult = this.getClassifierResult( + fileContext.leftFileContent, + fileContext.rightFileContent, + osPlatform, + autoTriggerType, + char, + lineNum, + fileContext.programmingLanguage + ) + + const threshold = this.getThreshold() + + const shouldTrigger = classifierResult > threshold + if (shouldRecordResult) { + TelemetryHelper.instance.setClassifierResult(classifierResult) + TelemetryHelper.instance.setClassifierThreshold(threshold) + } + return shouldTrigger + } + + private getClassifierResult( + leftContext: string, + rightContext: string, + os: string, + triggerType: string | undefined, + char: string, + lineNum: number, + language: ProgrammingLanguage + ): number { + const leftContextLines = leftContext.split(/\r?\n/) + const leftContextAtCurrentLine = leftContextLines[leftContextLines.length - 1] + const tokens = leftContextAtCurrentLine.trim().split(' ') + let keyword = '' + const lastToken = tokens[tokens.length - 1] + if (lastToken && lastToken.length > 1) { + keyword = lastToken + } + const lengthOfLeftCurrent = leftContextLines[leftContextLines.length - 1].length + const lengthOfLeftPrev = leftContextLines[leftContextLines.length - 2]?.length ?? 0 + const lengthOfRight = rightContext.trim().length + + const triggerTypeCoefficient: number = this.triggerTypeCoefficientMap[triggerType || ''] ?? 0 + const osCoefficient: number = this.osCoefficientMap[os] ?? 0 + const charCoefficient: number = this.charCoefficient[char] ?? 0 + const keyWordCoefficient: number = this.charCoefficient[keyword] ?? 0 + const ideCoefficient = this.ideVscode + + const previousDecision = TelemetryHelper.instance.getLastTriggerDecisionForClassifier() + const languageCoefficients = Object.values(this.languageCoefficientMap) + const avrgCoefficient = + languageCoefficients.length > 0 + ? languageCoefficients.reduce((a, b) => a + b) / languageCoefficients.length + : 0 + const languageCoefficient = this.languageCoefficientMap[language.languageName] ?? avrgCoefficient + + let previousDecisionCoefficient = 0 + if (previousDecision === 'Accept') { + previousDecisionCoefficient = this.prevDecisionAcceptCoefficient + } else if (previousDecision === 'Reject') { + previousDecisionCoefficient = this.prevDecisionRejectCoefficient + } else if (previousDecision === 'Discard' || previousDecision === 'Empty') { + previousDecisionCoefficient = this.prevDecisionOtherCoefficient + } + + let leftContextLengthCoefficient = 0 + if (leftContext.length >= 0 && leftContext.length < 5) { + leftContextLengthCoefficient = this.lengthLeft0To5 + } else if (leftContext.length >= 5 && leftContext.length < 10) { + leftContextLengthCoefficient = this.lengthLeft5To10 + } else if (leftContext.length >= 10 && leftContext.length < 20) { + leftContextLengthCoefficient = this.lengthLeft10To20 + } else if (leftContext.length >= 20 && leftContext.length < 30) { + leftContextLengthCoefficient = this.lengthLeft20To30 + } else if (leftContext.length >= 30 && leftContext.length < 40) { + leftContextLengthCoefficient = this.lengthLeft30To40 + } else if (leftContext.length >= 40 && leftContext.length < 50) { + leftContextLengthCoefficient = this.lengthLeft40To50 + } + + const result = + (this.lengthOfRightCoefficient * (lengthOfRight - this.minn.lenRight)) / + (this.maxx.lenRight - this.minn.lenRight) + + (this.lengthOfLeftCurrentCoefficient * (lengthOfLeftCurrent - this.minn.lenLeftCur)) / + (this.maxx.lenLeftCur - this.minn.lenLeftCur) + + (this.lengthOfLeftPrevCoefficient * (lengthOfLeftPrev - this.minn.lenLeftPrev)) / + (this.maxx.lenLeftPrev - this.minn.lenLeftPrev) + + (this.lineNumCoefficient * (lineNum - this.minn.lineNum)) / (this.maxx.lineNum - this.minn.lineNum) + + osCoefficient + + triggerTypeCoefficient + + charCoefficient + + keyWordCoefficient + + ideCoefficient + + this.intercept + + previousDecisionCoefficient + + languageCoefficient + + leftContextLengthCoefficient + + return sigmoid(result) + } + + private normalizeOsName(name: string, version: string | undefined): string { + const lowercaseName = name.toLowerCase() + if (lowercaseName.includes('windows')) { + if (!version) { + return 'Windows' + } else if (version.includes('Windows NT 10') || version.startsWith('10')) { + return 'Windows 10' + } else if (version.includes('6.1')) { + return 'Windows 7' + } else if (version.includes('6.3')) { + return 'Windows 8.1' + } else { + return 'Windows' + } + } else if ( + lowercaseName.includes('macos') || + lowercaseName.includes('mac os') || + lowercaseName.includes('darwin') + ) { + return 'Mac OS X' + } else if (lowercaseName.includes('linux')) { + return 'Linux' + } else { + return name + } + } +} + +const sigmoid = (x: number) => { + return 1 / (1 + Math.exp(-x)) +} diff --git a/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts b/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts new file mode 100644 index 00000000000..a6c424c321d --- /dev/null +++ b/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts @@ -0,0 +1,194 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import vscode, { Position } from 'vscode' +import { getPrefixSuffixOverlap } from '../util/commonUtil' +import { Recommendation } from '../client/codewhisperer' +import { session } from '../util/codeWhispererSession' +import { TelemetryHelper } from '../util/telemetryHelper' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { ReferenceInlineProvider } from './referenceInlineProvider' +import { ImportAdderProvider } from './importAdderProvider' +import { application } from '../util/codeWhispererApplication' +import path from 'path' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' + +export class CWInlineCompletionItemProvider implements vscode.InlineCompletionItemProvider { + private activeItemIndex: number | undefined + private nextMove: number + private recommendations: Recommendation[] + private requestId: string + private startPos: Position + private nextToken: string + + private _onDidShow: vscode.EventEmitter = new vscode.EventEmitter() + public readonly onDidShow: vscode.Event = this._onDidShow.event + + public constructor( + itemIndex: number | undefined, + firstMove: number, + recommendations: Recommendation[], + requestId: string, + startPos: Position, + nextToken: string + ) { + this.activeItemIndex = itemIndex + this.nextMove = firstMove + this.recommendations = recommendations + this.requestId = requestId + this.startPos = startPos + this.nextToken = nextToken + } + + get getActiveItemIndex() { + return this.activeItemIndex + } + + public clearActiveItemIndex() { + this.activeItemIndex = undefined + } + + // iterate suggestions and stop at index 0 or index len - 1 + private getIteratingIndexes() { + const len = this.recommendations.length + const startIndex = this.activeItemIndex ? this.activeItemIndex : 0 + const index = [] + if (this.nextMove === 0) { + for (let i = 0; i < len; i++) { + index.push((startIndex + i) % len) + } + } else if (this.nextMove === -1) { + for (let i = startIndex - 1; i >= 0; i--) { + index.push(i) + } + index.push(startIndex) + } else { + for (let i = startIndex + 1; i < len; i++) { + index.push(i) + } + index.push(startIndex) + } + return index + } + + truncateOverlapWithRightContext(document: vscode.TextDocument, suggestion: string, pos: vscode.Position): string { + const trimmedSuggestion = suggestion.trim() + // limit of 5000 for right context matching + const rightContext = document.getText(new vscode.Range(pos, document.positionAt(document.offsetAt(pos) + 5000))) + const overlap = getPrefixSuffixOverlap(trimmedSuggestion, rightContext) + const overlapIndex = suggestion.lastIndexOf(overlap) + if (overlapIndex >= 0) { + const truncated = suggestion.slice(0, overlapIndex) + return truncated.trim().length ? truncated : '' + } else { + return suggestion + } + } + + getInlineCompletionItem( + document: vscode.TextDocument, + r: Recommendation, + start: vscode.Position, + end: vscode.Position, + index: number, + prefix: string + ): vscode.InlineCompletionItem | undefined { + if (!r.content.startsWith(prefix)) { + return undefined + } + const effectiveStart = document.positionAt(document.offsetAt(start) + prefix.length) + const truncatedSuggestion = this.truncateOverlapWithRightContext(document, r.content, end) + if (truncatedSuggestion.length === 0) { + if (session.getSuggestionState(index) !== 'Showed') { + session.setSuggestionState(index, 'Discard') + } + return undefined + } + TelemetryHelper.instance.lastSuggestionInDisplay = truncatedSuggestion + return { + insertText: truncatedSuggestion, + range: new vscode.Range(start, end), + command: { + command: 'aws.amazonq.accept', + title: 'On acceptance', + arguments: [ + new vscode.Range(start, end), + new vscode.Range(effectiveStart, end), + index, + truncatedSuggestion, + this.requestId, + session.sessionId, + session.triggerType, + session.getCompletionType(index), + runtimeLanguageContext.getLanguageContext(document.languageId, path.extname(document.fileName)) + .language, + r.references, + ], + }, + } + } + + // the returned completion items will always only contain one valid item + // this is to trace the current index of visible completion item + // so that reference tracker can show + // This hack can be removed once inlineCompletionAdditions API becomes public + provideInlineCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + _context: vscode.InlineCompletionContext, + _token: vscode.CancellationToken + ): vscode.ProviderResult { + if (position.line < 0 || position.isBefore(this.startPos)) { + application()._clearCodeWhispererUIListener.fire() + this.activeItemIndex = undefined + return + } + + // There's a chance that the startPos is no longer valid in the current document (e.g. + // when CodeWhisperer got triggered by 'Enter', the original startPos is with indentation + // but then this indentation got removed by VSCode when another new line is inserted, + // before the code reaches here). In such case, we need to update the startPos to be a + // valid one. Otherwise, inline completion which utilizes this position will function + // improperly. + const start = document.validatePosition(this.startPos) + const end = position + const iteratingIndexes = this.getIteratingIndexes() + const prefix = document.getText(new vscode.Range(start, end)).replace(/\r\n/g, '\n') + const matchedCount = session.recommendations.filter( + (r) => r.content.length > 0 && r.content.startsWith(prefix) && r.content !== prefix + ).length + for (const i of iteratingIndexes) { + const r = session.recommendations[i] + const item = this.getInlineCompletionItem(document, r, start, end, i, prefix) + if (item === undefined) { + continue + } + this.activeItemIndex = i + session.setSuggestionState(i, 'Showed') + ReferenceInlineProvider.instance.setInlineReference(this.startPos.line, r.content, r.references) + ImportAdderProvider.instance.onShowRecommendation(document, this.startPos.line, r) + this.nextMove = 0 + TelemetryHelper.instance.setFirstSuggestionShowTime() + session.setPerceivedLatency() + UserWrittenCodeTracker.instance.onQStartsMakingEdits() + this._onDidShow.fire() + if (matchedCount >= 2 || this.nextToken !== '') { + const result = [item] + for (let j = 0; j < matchedCount - 1; j++) { + result.push({ + insertText: `${ + typeof item.insertText === 'string' ? item.insertText : item.insertText.value + }${j}`, + range: item.range, + }) + } + return result + } + return [item] + } + application()._clearCodeWhispererUIListener.fire() + this.activeItemIndex = undefined + return [] + } +} diff --git a/packages/core/src/codewhisperer/service/inlineCompletionService.ts b/packages/core/src/codewhisperer/service/inlineCompletionService.ts new file mode 100644 index 00000000000..cd37663af49 --- /dev/null +++ b/packages/core/src/codewhisperer/service/inlineCompletionService.ts @@ -0,0 +1,163 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { CodeSuggestionsState, ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' +import * as CodeWhispererConstants from '../models/constants' +import { DefaultCodeWhispererClient } from '../client/codewhisperer' +import { RecommendationHandler } from './recommendationHandler' +import { CodewhispererAutomatedTriggerType, CodewhispererTriggerType } from '../../shared/telemetry/telemetry' +import { showTimedMessage } from '../../shared/utilities/messages' +import { getLogger } from '../../shared/logger/logger' +import { TelemetryHelper } from '../util/telemetryHelper' +import { AuthUtil } from '../util/authUtil' +import { shared } from '../../shared/utilities/functionUtils' +import { ClassifierTrigger } from './classifierTrigger' +import { session } from '../util/codeWhispererSession' +import { noSuggestions } from '../models/constants' +import { CodeWhispererStatusBarManager } from './statusBar' + +export class InlineCompletionService { + private maxPage = 100 + private statusBar: CodeWhispererStatusBarManager + private _showRecommendationTimer?: NodeJS.Timer + + constructor(statusBar: CodeWhispererStatusBarManager = CodeWhispererStatusBarManager.instance) { + this.statusBar = statusBar + + RecommendationHandler.instance.onDidReceiveRecommendation((e) => { + this.startShowRecommendationTimer() + }) + + CodeSuggestionsState.instance.onDidChangeState(() => { + return this.statusBar.refreshStatusBar() + }) + } + + static #instance: InlineCompletionService + + public static get instance() { + return (this.#instance ??= new this()) + } + + filePath(): string | undefined { + return RecommendationHandler.instance.documentUri?.fsPath + } + + private sharedTryShowRecommendation = shared( + RecommendationHandler.instance.tryShowRecommendation.bind(RecommendationHandler.instance) + ) + + private startShowRecommendationTimer() { + if (this._showRecommendationTimer) { + clearInterval(this._showRecommendationTimer) + this._showRecommendationTimer = undefined + } + this._showRecommendationTimer = setInterval(() => { + const delay = Date.now() - vsCodeState.lastUserModificationTime + if (delay < CodeWhispererConstants.inlineSuggestionShowDelay) { + return + } + this.sharedTryShowRecommendation() + .catch((e) => { + getLogger().error('tryShowRecommendation failed: %s', (e as Error).message) + }) + .finally(() => { + if (this._showRecommendationTimer) { + clearInterval(this._showRecommendationTimer) + this._showRecommendationTimer = undefined + } + }) + }, CodeWhispererConstants.showRecommendationTimerPollPeriod) + } + + async getPaginatedRecommendation( + client: DefaultCodeWhispererClient, + editor: vscode.TextEditor, + triggerType: CodewhispererTriggerType, + config: ConfigurationEntry, + autoTriggerType?: CodewhispererAutomatedTriggerType, + event?: vscode.TextDocumentChangeEvent + ): Promise { + if (vsCodeState.isCodeWhispererEditing || RecommendationHandler.instance.isSuggestionVisible()) { + return { + result: 'Failed', + errorMessage: 'Amazon Q is already running', + recommendationCount: 0, + } + } + + // Call report user decisions once to report recommendations leftover from last invocation. + RecommendationHandler.instance.reportUserDecisions(-1) + TelemetryHelper.instance.setInvokeSuggestionStartTime() + ClassifierTrigger.instance.recordClassifierResultForAutoTrigger(editor, autoTriggerType, event) + + const triggerChar = event?.contentChanges[0]?.text + if (autoTriggerType === 'SpecialCharacters' && triggerChar) { + TelemetryHelper.instance.setTriggerCharForUserTriggerDecision(triggerChar) + } + const isAutoTrigger = triggerType === 'AutoTrigger' + if (AuthUtil.instance.isConnectionExpired()) { + await AuthUtil.instance.notifyReauthenticate(isAutoTrigger) + return { + result: 'Failed', + errorMessage: 'auth', + recommendationCount: 0, + } + } + + await this.statusBar.setLoading() + + RecommendationHandler.instance.checkAndResetCancellationTokens() + RecommendationHandler.instance.documentUri = editor.document.uri + let response: GetRecommendationsResponse = { + result: 'Failed', + errorMessage: undefined, + recommendationCount: 0, + } + try { + let page = 0 + while (page < this.maxPage) { + response = await RecommendationHandler.instance.getRecommendations( + client, + editor, + triggerType, + config, + autoTriggerType, + true, + page + ) + if (RecommendationHandler.instance.checkAndResetCancellationTokens()) { + RecommendationHandler.instance.reportUserDecisions(-1) + await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') + if (triggerType === 'OnDemand' && session.recommendations.length === 0) { + void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 2000) + } + return { + result: 'Failed', + errorMessage: 'cancelled', + recommendationCount: 0, + } + } + if (!RecommendationHandler.instance.hasNextToken()) { + break + } + page++ + } + } catch (error) { + getLogger().error(`Error ${error} in getPaginatedRecommendation`) + } + await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') + if (triggerType === 'OnDemand' && session.recommendations.length === 0) { + void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 2000) + } + TelemetryHelper.instance.tryRecordClientComponentLatency() + + return { + result: 'Succeeded', + errorMessage: undefined, + recommendationCount: session.recommendations.length, + } + } +} diff --git a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts new file mode 100644 index 00000000000..312e31c248a --- /dev/null +++ b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts @@ -0,0 +1,267 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { DefaultCodeWhispererClient } from '../client/codewhisperer' +import * as CodeWhispererConstants from '../models/constants' +import { ConfigurationEntry } from '../models/model' +import { getLogger } from '../../shared/logger/logger' +import { RecommendationHandler } from './recommendationHandler' +import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' +import { getTabSizeSetting } from '../../shared/utilities/editorUtilities' +import { isInlineCompletionEnabled } from '../util/commonUtil' +import { ClassifierTrigger } from './classifierTrigger' +import { extractContextForCodeWhisperer } from '../util/editorContext' +import { RecommendationService } from './recommendationService' + +/** + * This class is for CodeWhisperer auto trigger + */ +export class KeyStrokeHandler { + /** + * Special character which automated triggers codewhisperer + */ + public specialChar: string + /** + * Key stroke count for automated trigger + */ + + private idleTriggerTimer?: NodeJS.Timer + + public lastInvocationTime?: number + + constructor() { + this.specialChar = '' + } + + static #instance: KeyStrokeHandler + + public static get instance() { + return (this.#instance ??= new this()) + } + + public startIdleTimeTriggerTimer( + event: vscode.TextDocumentChangeEvent, + editor: vscode.TextEditor, + client: DefaultCodeWhispererClient, + config: ConfigurationEntry + ) { + if (this.idleTriggerTimer) { + clearInterval(this.idleTriggerTimer) + this.idleTriggerTimer = undefined + } + if (!this.shouldTriggerIdleTime()) { + return + } + this.idleTriggerTimer = setInterval(() => { + const duration = (Date.now() - RecommendationHandler.instance.lastInvocationTime) / 1000 + if (duration < CodeWhispererConstants.invocationTimeIntervalThreshold) { + return + } + + this.invokeAutomatedTrigger('IdleTime', editor, client, config, event) + .catch((e) => { + getLogger().error('invokeAutomatedTrigger failed: %s', (e as Error).message) + }) + .finally(() => { + if (this.idleTriggerTimer) { + clearInterval(this.idleTriggerTimer) + this.idleTriggerTimer = undefined + } + }) + }, CodeWhispererConstants.idleTimerPollPeriod) + } + + public shouldTriggerIdleTime(): boolean { + if (isInlineCompletionEnabled() && RecommendationService.instance.isRunning) { + return false + } + return true + } + + async processKeyStroke( + event: vscode.TextDocumentChangeEvent, + editor: vscode.TextEditor, + client: DefaultCodeWhispererClient, + config: ConfigurationEntry + ): Promise { + try { + if (!config.isAutomatedTriggerEnabled) { + return + } + + // Skip when output channel gains focus and invoke + if (editor.document.languageId === 'Log') { + return + } + + const { rightFileContent } = extractContextForCodeWhisperer(editor) + const rightContextLines = rightFileContent.split(/\r?\n/) + const rightContextAtCurrentLine = rightContextLines[0] + // we do not want to trigger when there is immediate right context on the same line + // with "}" being an exception because of IDE auto-complete + if ( + rightContextAtCurrentLine.length && + !rightContextAtCurrentLine.startsWith(' ') && + rightContextAtCurrentLine.trim() !== '}' && + rightContextAtCurrentLine.trim() !== ')' + ) { + return + } + + let triggerType: CodewhispererAutomatedTriggerType | undefined + const changedSource = new DefaultDocumentChangedType(event.contentChanges).checkChangeSource() + + switch (changedSource) { + case DocumentChangedSource.EnterKey: { + triggerType = 'Enter' + break + } + case DocumentChangedSource.SpecialCharsKey: { + triggerType = 'SpecialCharacters' + break + } + case DocumentChangedSource.RegularKey: { + triggerType = ClassifierTrigger.instance.shouldTriggerFromClassifier(event, editor, triggerType) + ? 'Classifier' + : undefined + break + } + default: { + break + } + } + + if (triggerType) { + await this.invokeAutomatedTrigger(triggerType, editor, client, config, event) + } + } catch (error) { + getLogger().verbose(`Automated Trigger Exception : ${error}`) + } + } + + async invokeAutomatedTrigger( + autoTriggerType: CodewhispererAutomatedTriggerType, + editor: vscode.TextEditor, + client: DefaultCodeWhispererClient, + config: ConfigurationEntry, + event: vscode.TextDocumentChangeEvent + ): Promise { + if (!editor) { + return + } + + // RecommendationHandler.instance.reportUserDecisionOfRecommendation(editor, -1) + await RecommendationService.instance.generateRecommendation( + client, + editor, + 'AutoTrigger', + config, + autoTriggerType + ) + } +} + +export abstract class DocumentChangedType { + constructor(protected readonly contentChanges: ReadonlyArray) { + this.contentChanges = contentChanges + } + + abstract checkChangeSource(): DocumentChangedSource + + // Enter key should always start with ONE '\n' or '\r\n' and potentially following spaces due to IDE reformat + protected isEnterKey(str: string): boolean { + if (str.length === 0) { + return false + } + return ( + (str.startsWith('\r\n') && str.substring(2).trim() === '') || + (str[0] === '\n' && str.substring(1).trim() === '') + ) + } + + // Tab should consist of space char only ' ' and the length % tabSize should be 0 + protected isTabKey(str: string): boolean { + const tabSize = getTabSizeSetting() + if (str.length % tabSize === 0 && str.trim() === '') { + return true + } + return false + } + + protected isUserTypingSpecialChar(str: string): boolean { + return ['(', '()', '[', '[]', '{', '{}', ':'].includes(str) + } + + protected isSingleLine(str: string): boolean { + let newLineCounts = 0 + for (const ch of str) { + if (ch === '\n') { + newLineCounts += 1 + } + } + + // since pressing Enter key possibly will generate string like '\n ' due to indention + if (this.isEnterKey(str)) { + return true + } + if (newLineCounts >= 1) { + return false + } + return true + } +} + +export class DefaultDocumentChangedType extends DocumentChangedType { + constructor(contentChanges: ReadonlyArray) { + super(contentChanges) + } + + checkChangeSource(): DocumentChangedSource { + if (this.contentChanges.length === 0) { + return DocumentChangedSource.Unknown + } + + // event.contentChanges.length will be 2 when user press Enter key multiple times + if (this.contentChanges.length > 2) { + return DocumentChangedSource.Reformatting + } + + // Case when event.contentChanges.length === 1 + const changedText = this.contentChanges[0].text + + if (this.isSingleLine(changedText)) { + if (changedText === '') { + return DocumentChangedSource.Deletion + } else if (this.isEnterKey(changedText)) { + return DocumentChangedSource.EnterKey + } else if (this.isTabKey(changedText)) { + return DocumentChangedSource.TabKey + } else if (this.isUserTypingSpecialChar(changedText)) { + return DocumentChangedSource.SpecialCharsKey + } else if (changedText.length === 1) { + return DocumentChangedSource.RegularKey + } else if (new RegExp('^[ ]+$').test(changedText)) { + // single line && single place reformat should consist of space chars only + return DocumentChangedSource.Reformatting + } else { + return DocumentChangedSource.Unknown + } + } + + // Won't trigger cwspr on multi-line changes + return DocumentChangedSource.Unknown + } +} + +export enum DocumentChangedSource { + SpecialCharsKey = 'SpecialCharsKey', + RegularKey = 'RegularKey', + TabKey = 'TabKey', + EnterKey = 'EnterKey', + Reformatting = 'Reformatting', + Deletion = 'Deletion', + Unknown = 'Unknown', +} diff --git a/packages/core/src/codewhisperer/service/recommendationHandler.ts b/packages/core/src/codewhisperer/service/recommendationHandler.ts new file mode 100644 index 00000000000..1cd64ccbe4b --- /dev/null +++ b/packages/core/src/codewhisperer/service/recommendationHandler.ts @@ -0,0 +1,731 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { extensionVersion } from '../../shared/vscode/env' +import { RecommendationsList, DefaultCodeWhispererClient, CognitoCredentialsError } from '../client/codewhisperer' +import * as EditorContext from '../util/editorContext' +import * as CodeWhispererConstants from '../models/constants' +import { ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { ServiceException } from '@smithy/smithy-client' +import { isServiceException } from '../../shared/errors' +import { TelemetryHelper } from '../util/telemetryHelper' +import { getLogger } from '../../shared/logger/logger' +import { hasVendedIamCredentials } from '../../auth/auth' +import { + asyncCallWithTimeout, + isInlineCompletionEnabled, + isVscHavingRegressionInlineCompletionApi, +} from '../util/commonUtil' +import { showTimedMessage } from '../../shared/utilities/messages' +import { + CodewhispererAutomatedTriggerType, + CodewhispererCompletionType, + CodewhispererGettingStartedTask, + CodewhispererTriggerType, + telemetry, +} from '../../shared/telemetry/telemetry' +import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' +import { invalidCustomizationMessage } from '../models/constants' +import { getSelectedCustomization, switchToBaseCustomizationAndNotify } from '../util/customizationUtil' +import { session } from '../util/codeWhispererSession' +import { Commands } from '../../shared/vscode/commands2' +import globals from '../../shared/extensionGlobals' +import { noSuggestions, updateInlineLockKey } from '../models/constants' +import AsyncLock from 'async-lock' +import { AuthUtil } from '../util/authUtil' +import { CWInlineCompletionItemProvider } from './inlineCompletionItemProvider' +import { application } from '../util/codeWhispererApplication' +import { openUrl } from '../../shared/utilities/vsCodeUtils' +import { indent } from '../../shared/utilities/textUtilities' +import path from 'path' +import { isIamConnection } from '../../auth/connection' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' +import { BaseLanguageClient } from 'vscode-languageclient' + +/** + * This class is for getRecommendation/listRecommendation API calls and its states + * It does not contain UI/UX related logic + */ + +/** + * Commands as a level of indirection so that declare doesn't intercept any registrations for the + * language server implementation. + * + * Otherwise you'll get: + * "Unable to launch amazonq language server: Command "aws.amazonq.rejectCodeSuggestion" has already been declared by the Toolkit" + */ +function createCommands() { + // below commands override VS Code inline completion commands + const prevCommand = Commands.declare('editor.action.inlineSuggest.showPrevious', () => async () => { + await RecommendationHandler.instance.showRecommendation(-1) + }) + const nextCommand = Commands.declare('editor.action.inlineSuggest.showNext', () => async () => { + await RecommendationHandler.instance.showRecommendation(1) + }) + + const rejectCommand = Commands.declare('aws.amazonq.rejectCodeSuggestion', () => async () => { + telemetry.record({ + traceId: TelemetryHelper.instance.traceId, + }) + + await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') + RecommendationHandler.instance.reportUserDecisions(-1) + await Commands.tryExecute('aws.amazonq.refreshAnnotation') + }) + + return { + prevCommand, + nextCommand, + rejectCommand, + } +} + +const lock = new AsyncLock({ maxPending: 1 }) + +export class RecommendationHandler { + public lastInvocationTime: number + // TODO: remove this requestId + public requestId: string + private nextToken: string + private cancellationToken: vscode.CancellationTokenSource + private _onDidReceiveRecommendation: vscode.EventEmitter = new vscode.EventEmitter() + public readonly onDidReceiveRecommendation: vscode.Event = this._onDidReceiveRecommendation.event + private inlineCompletionProvider?: CWInlineCompletionItemProvider + private inlineCompletionProviderDisposable?: vscode.Disposable + private reject: vscode.Disposable + private next: vscode.Disposable + private prev: vscode.Disposable + private _timer?: NodeJS.Timer + private languageClient?: BaseLanguageClient + documentUri: vscode.Uri | undefined = undefined + + constructor() { + this.requestId = '' + this.nextToken = '' + this.lastInvocationTime = Date.now() - CodeWhispererConstants.invocationTimeIntervalThreshold * 1000 + this.cancellationToken = new vscode.CancellationTokenSource() + this.prev = new vscode.Disposable(() => {}) + this.next = new vscode.Disposable(() => {}) + this.reject = new vscode.Disposable(() => {}) + } + + static #instance: RecommendationHandler + + public static get instance() { + return (this.#instance ??= new this()) + } + + isValidResponse(): boolean { + return session.recommendations.some((r) => r.content.trim() !== '') + } + + setLanguageClient(languageClient: BaseLanguageClient) { + this.languageClient = languageClient + } + + async getServerResponse( + triggerType: CodewhispererTriggerType, + isManualTriggerOn: boolean, + promise: Promise + ): Promise { + const timeoutMessage = hasVendedIamCredentials() + ? 'Generate recommendation timeout.' + : 'List recommendation timeout' + if (isManualTriggerOn && triggerType === 'OnDemand' && hasVendedIamCredentials()) { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: CodeWhispererConstants.pendingResponse, + cancellable: false, + }, + async () => { + return await asyncCallWithTimeout( + promise, + timeoutMessage, + CodeWhispererConstants.promiseTimeoutLimit * 1000 + ) + } + ) + } + return await asyncCallWithTimeout(promise, timeoutMessage, CodeWhispererConstants.promiseTimeoutLimit * 1000) + } + + async getTaskTypeFromEditorFileName(filePath: string): Promise { + if (filePath.includes('CodeWhisperer_generate_suggestion')) { + return 'autoTrigger' + } else if (filePath.includes('CodeWhisperer_manual_invoke')) { + return 'manualTrigger' + } else if (filePath.includes('CodeWhisperer_use_comments')) { + return 'commentAsPrompt' + } else if (filePath.includes('CodeWhisperer_navigate_suggestions')) { + return 'navigation' + } else if (filePath.includes('Generate_unit_tests')) { + return 'unitTest' + } else { + return undefined + } + } + + async getRecommendations( + client: DefaultCodeWhispererClient, + editor: vscode.TextEditor, + triggerType: CodewhispererTriggerType, + config: ConfigurationEntry, + autoTriggerType?: CodewhispererAutomatedTriggerType, + pagination: boolean = true, + page: number = 0, + generate: boolean = isIamConnection(AuthUtil.instance.conn) + ): Promise { + let invocationResult: 'Succeeded' | 'Failed' = 'Failed' + let errorMessage: string | undefined = undefined + let errorCode: string | undefined = undefined + + if (!editor) { + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + recommendationCount: 0, + }) + } + let recommendations: RecommendationsList = [] + let requestId = '' + let sessionId = '' + let reason = '' + let startTime = 0 + let latency = 0 + let nextToken = '' + let shouldRecordServiceInvocation = true + session.language = runtimeLanguageContext.getLanguageContext( + editor.document.languageId, + path.extname(editor.document.fileName) + ).language + session.taskType = await this.getTaskTypeFromEditorFileName(editor.document.fileName) + + if (pagination && !generate) { + if (page === 0) { + session.requestContext = await EditorContext.buildListRecommendationRequest( + editor as vscode.TextEditor, + this.nextToken, + config.isSuggestionsWithCodeReferencesEnabled, + this.languageClient + ) + } else { + session.requestContext = { + request: { + ...session.requestContext.request, + // Putting nextToken assignment in the end so it overwrites the existing nextToken + nextToken: this.nextToken, + }, + supplementalMetadata: session.requestContext.supplementalMetadata, + } + } + } else { + session.requestContext = await EditorContext.buildGenerateRecommendationRequest(editor as vscode.TextEditor) + } + const request = session.requestContext.request + // record preprocessing end time + TelemetryHelper.instance.setPreprocessEndTime() + + // set start pos for non pagination call or first pagination call + if (!pagination || (pagination && page === 0)) { + session.startPos = editor.selection.active + session.startCursorOffset = editor.document.offsetAt(session.startPos) + session.leftContextOfCurrentLine = EditorContext.getLeftContext(editor, session.startPos.line) + session.triggerType = triggerType + session.autoTriggerType = autoTriggerType + + /** + * Validate request + */ + if (!EditorContext.validateRequest(request)) { + getLogger().verbose('Invalid Request: %O', request) + const languageName = request.fileContext.programmingLanguage.languageName + if (!runtimeLanguageContext.isLanguageSupported(languageName)) { + errorMessage = `${languageName} is currently not supported by Amazon Q inline suggestions` + } + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + recommendationCount: 0, + }) + } + } + + try { + startTime = Date.now() + this.lastInvocationTime = startTime + const mappedReq = runtimeLanguageContext.mapToRuntimeLanguage(request) + const codewhispererPromise = + pagination && !generate + ? client.listRecommendations(mappedReq) + : client.generateRecommendations(mappedReq) + const resp = await this.getServerResponse(triggerType, config.isManualTriggerEnabled, codewhispererPromise) + TelemetryHelper.instance.setSdkApiCallEndTime() + latency = startTime !== 0 ? Date.now() - startTime : 0 + if ('recommendations' in resp) { + recommendations = (resp && resp.recommendations) || [] + } else { + recommendations = (resp && resp.completions) || [] + } + invocationResult = 'Succeeded' + requestId = resp?.$response && resp?.$response?.requestId + nextToken = resp?.nextToken ? resp?.nextToken : '' + sessionId = resp?.$response?.httpResponse?.headers['x-amzn-sessionid'] + TelemetryHelper.instance.setFirstResponseRequestId(requestId) + if (page === 0) { + session.setTimeToFirstRecommendation(Date.now()) + } + if (nextToken === '') { + TelemetryHelper.instance.setAllPaginationEndTime() + } + } catch (error) { + if (error instanceof CognitoCredentialsError) { + shouldRecordServiceInvocation = false + } + if (latency === 0) { + latency = startTime !== 0 ? Date.now() - startTime : 0 + } + getLogger().error('amazonq inline-suggest: Invocation Exception : %s', (error as Error).message) + if (isServiceException(error)) { + errorMessage = error.message + requestId = error.$metadata.requestId || '' + errorCode = error.name + reason = `CodeWhisperer Invocation Exception: ${error?.name ?? 'unknown'}` + await this.onThrottlingException(error, triggerType) + + if (error?.name === 'AccessDeniedException' && errorMessage?.includes('no identity-based policy')) { + getLogger().error('amazonq inline-suggest: AccessDeniedException : %s', (error as Error).message) + void vscode.window + .showErrorMessage(`CodeWhisperer: ${error?.message}`, CodeWhispererConstants.settingsLearnMore) + .then(async (resp) => { + if (resp === CodeWhispererConstants.settingsLearnMore) { + void openUrl(vscode.Uri.parse(CodeWhispererConstants.learnMoreUri)) + } + }) + await vscode.commands.executeCommand('aws.amazonq.enableCodeSuggestions', false) + } + } else { + errorMessage = error instanceof Error ? error.message : String(error) + reason = error ? String(error) : 'unknown' + } + } finally { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone + + let msg = indent( + `codewhisperer: request-id: ${requestId}, + timestamp(epoch): ${Date.now()}, + timezone: ${timezone}, + datetime: ${new Date().toLocaleString([], { timeZone: timezone })}, + vscode version: '${vscode.version}', + extension version: '${extensionVersion}', + filename: '${EditorContext.getFileName(editor)}', + left context of line: '${session.leftContextOfCurrentLine}', + line number: ${session.startPos.line}, + character location: ${session.startPos.character}, + latency: ${latency} ms. + Recommendations:`, + 4, + true + ).trimStart() + for (const [index, item] of recommendations.entries()) { + msg += `\n ${index.toString().padStart(2, '0')}: ${indent(item.content, 8, true).trim()}` + session.requestIdList.push(requestId) + } + getLogger().debug(msg) + if (invocationResult === 'Succeeded') { + CodeWhispererCodeCoverageTracker.getTracker(session.language)?.incrementServiceInvocationCount() + UserWrittenCodeTracker.instance.onQFeatureInvoked() + } else { + if ( + (errorMessage?.includes(invalidCustomizationMessage) && errorCode === 'AccessDeniedException') || + errorCode === 'ResourceNotFoundException' + ) { + getLogger() + .debug(`The selected customization is no longer available. Retrying with the default model. + Failed request id: ${requestId}`) + await switchToBaseCustomizationAndNotify() + await this.getRecommendations( + client, + editor, + triggerType, + config, + autoTriggerType, + pagination, + page, + true + ) + } + } + + if (shouldRecordServiceInvocation) { + TelemetryHelper.instance.recordServiceInvocationTelemetry( + requestId, + sessionId, + session.recommendations.length + recommendations.length - 1, + invocationResult, + latency, + session.language, + session.taskType, + reason, + session.requestContext.supplementalMetadata + ) + } + } + + if (this.isCancellationRequested()) { + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + recommendationCount: session.recommendations.length, + }) + } + + const typedPrefix = editor.document + .getText(new vscode.Range(session.startPos, editor.selection.active)) + .replace('\r\n', '\n') + if (recommendations.length > 0) { + TelemetryHelper.instance.setTypeAheadLength(typedPrefix.length) + // mark suggestions that does not match typeahead when arrival as Discard + // these suggestions can be marked as Showed if typeahead can be removed with new inline API + for (const [i, r] of recommendations.entries()) { + const recommendationIndex = i + session.recommendations.length + if ( + !r.content.startsWith(typedPrefix) && + session.getSuggestionState(recommendationIndex) === undefined + ) { + session.setSuggestionState(recommendationIndex, 'Discard') + } + session.setCompletionType(recommendationIndex, r) + } + session.recommendations = pagination ? session.recommendations.concat(recommendations) : recommendations + if (isInlineCompletionEnabled() && this.hasAtLeastOneValidSuggestion(typedPrefix)) { + this._onDidReceiveRecommendation.fire() + } + } + + this.requestId = requestId + session.sessionId = sessionId + this.nextToken = nextToken + + // send Empty userDecision event if user receives no recommendations in this session at all. + if (invocationResult === 'Succeeded' && nextToken === '') { + // case 1: empty list of suggestion [] + if (session.recommendations.length === 0) { + session.requestIdList.push(requestId) + // Received an empty list of recommendations + TelemetryHelper.instance.recordUserDecisionTelemetryForEmptyList( + session.requestIdList, + sessionId, + page, + runtimeLanguageContext.getLanguageContext( + editor.document.languageId, + path.extname(editor.document.fileName) + ).language, + session.requestContext.supplementalMetadata + ) + } + // case 2: non empty list of suggestion but with (a) empty content or (b) non-matching typeahead + else if (!this.hasAtLeastOneValidSuggestion(typedPrefix)) { + this.reportUserDecisions(-1) + } + } + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + recommendationCount: session.recommendations.length, + }) + } + + hasAtLeastOneValidSuggestion(typedPrefix: string): boolean { + return session.recommendations.some((r) => r.content.trim() !== '' && r.content.startsWith(typedPrefix)) + } + + cancelPaginatedRequest() { + this.nextToken = '' + this.cancellationToken.cancel() + } + + isCancellationRequested() { + return this.cancellationToken.token.isCancellationRequested + } + + checkAndResetCancellationTokens() { + if (this.isCancellationRequested()) { + this.cancellationToken.dispose() + this.cancellationToken = new vscode.CancellationTokenSource() + this.nextToken = '' + return true + } + return false + } + /** + * Clear recommendation state + */ + clearRecommendations() { + session.requestIdList = [] + session.recommendations = [] + session.suggestionStates = new Map() + session.completionTypes = new Map() + this.requestId = '' + session.sessionId = '' + this.nextToken = '' + session.requestContext.supplementalMetadata = undefined + } + + async clearInlineCompletionStates() { + try { + vsCodeState.isCodeWhispererEditing = false + application()._clearCodeWhispererUIListener.fire() + this.cancelPaginatedRequest() + this.clearRecommendations() + this.disposeInlineCompletion() + await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') + // fix a regression that requires user to hit Esc twice to clear inline ghost text + // because disposing a provider does not clear the UX + if (isVscHavingRegressionInlineCompletionApi()) { + await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') + } + } finally { + this.clearRejectionTimer() + } + } + + reportDiscardedUserDecisions() { + for (const [i, _] of session.recommendations.entries()) { + session.setSuggestionState(i, 'Discard') + } + this.reportUserDecisions(-1) + } + + /** + * Emits telemetry reflecting user decision for current recommendation. + */ + reportUserDecisions(acceptIndex: number) { + if (session.sessionId === '' || this.requestId === '') { + return + } + TelemetryHelper.instance.recordUserDecisionTelemetry( + session.requestIdList, + session.sessionId, + session.recommendations, + acceptIndex, + session.recommendations.length, + session.completionTypes, + session.suggestionStates, + session.requestContext.supplementalMetadata + ) + if (isInlineCompletionEnabled()) { + this.clearInlineCompletionStates().catch((e) => { + getLogger().error('clearInlineCompletionStates failed: %s', (e as Error).message) + }) + } + } + + hasNextToken(): boolean { + return this.nextToken !== '' + } + + canShowRecommendationInIntelliSense( + editor: vscode.TextEditor, + showPrompt: boolean = false, + response: GetRecommendationsResponse + ): boolean { + const reject = () => { + this.reportUserDecisions(-1) + } + if (!this.isValidResponse()) { + if (showPrompt) { + void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 3000) + } + reject() + return false + } + // do not show recommendation if cursor is before invocation position + // also mark as Discard + if (editor.selection.active.isBefore(session.startPos)) { + for (const [i, _] of session.recommendations.entries()) { + session.setSuggestionState(i, 'Discard') + } + reject() + return false + } + + // do not show recommendation if typeahead does not match + // also mark as Discard + const typedPrefix = editor.document.getText( + new vscode.Range( + session.startPos.line, + session.startPos.character, + editor.selection.active.line, + editor.selection.active.character + ) + ) + if (!session.recommendations[0].content.startsWith(typedPrefix.trimStart())) { + for (const [i, _] of session.recommendations.entries()) { + session.setSuggestionState(i, 'Discard') + } + reject() + return false + } + return true + } + + async onThrottlingException(awsError: ServiceException, triggerType: CodewhispererTriggerType) { + if ( + awsError.name === 'ThrottlingException' && + awsError.message.includes(CodeWhispererConstants.throttlingMessage) + ) { + if (triggerType === 'OnDemand') { + void vscode.window.showErrorMessage(CodeWhispererConstants.freeTierLimitReached) + } + vsCodeState.isFreeTierLimitReached = true + } + } + + public disposeInlineCompletion() { + this.inlineCompletionProviderDisposable?.dispose() + this.inlineCompletionProvider = undefined + } + + private disposeCommandOverrides() { + this.prev.dispose() + this.reject.dispose() + this.next.dispose() + } + + // These commands override the vs code inline completion commands + // They are subscribed when suggestion starts and disposed when suggestion is accepted/rejected + // to avoid impacting other plugins or user who uses this API + private registerCommandOverrides() { + const { prevCommand, nextCommand, rejectCommand } = createCommands() + this.prev = prevCommand.register() + this.next = nextCommand.register() + this.reject = rejectCommand.register() + } + + subscribeSuggestionCommands() { + this.disposeCommandOverrides() + this.registerCommandOverrides() + globals.context.subscriptions.push(this.prev) + globals.context.subscriptions.push(this.next) + globals.context.subscriptions.push(this.reject) + } + + async showRecommendation(indexShift: number, noSuggestionVisible: boolean = false) { + await lock.acquire(updateInlineLockKey, async () => { + if (!vscode.window.state.focused) { + this.reportDiscardedUserDecisions() + return + } + const inlineCompletionProvider = new CWInlineCompletionItemProvider( + this.inlineCompletionProvider?.getActiveItemIndex, + indexShift, + session.recommendations, + this.requestId, + session.startPos, + this.nextToken + ) + this.inlineCompletionProviderDisposable?.dispose() + // when suggestion is active, registering a new provider will let VS Code invoke inline API automatically + this.inlineCompletionProviderDisposable = vscode.languages.registerInlineCompletionItemProvider( + Object.assign([], CodeWhispererConstants.platformLanguageIds), + inlineCompletionProvider + ) + this.inlineCompletionProvider = inlineCompletionProvider + + if (isVscHavingRegressionInlineCompletionApi() && !noSuggestionVisible) { + // fix a regression in new VS Code when disposing and re-registering + // a new provider does not auto refresh the inline suggestion widget + // by manually refresh it + await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + } + if (noSuggestionVisible) { + await vscode.commands.executeCommand(`editor.action.inlineSuggest.trigger`) + this.sendPerceivedLatencyTelemetry() + } + }) + } + + async onEditorChange() { + this.reportUserDecisions(-1) + } + + async onFocusChange() { + this.reportUserDecisions(-1) + } + + async onCursorChange(e: vscode.TextEditorSelectionChangeEvent) { + // we do not want to reset the states for keyboard events because they can be typeahead + if ( + e.kind !== vscode.TextEditorSelectionChangeKind.Keyboard && + vscode.window.activeTextEditor === e.textEditor + ) { + application()._clearCodeWhispererUIListener.fire() + // when cursor change due to mouse movement we need to reset the active item index for inline + if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse) { + this.inlineCompletionProvider?.clearActiveItemIndex() + } + } + } + + isSuggestionVisible(): boolean { + return this.inlineCompletionProvider?.getActiveItemIndex !== undefined + } + + async tryShowRecommendation() { + const editor = vscode.window.activeTextEditor + if (editor === undefined) { + return + } + if (this.isSuggestionVisible()) { + // do not force refresh the tooltip to avoid suggestion "flashing" + return + } + if ( + editor.selection.active.isBefore(session.startPos) || + editor.document.uri.fsPath !== this.documentUri?.fsPath + ) { + for (const [i, _] of session.recommendations.entries()) { + session.setSuggestionState(i, 'Discard') + } + this.reportUserDecisions(-1) + } else if (session.recommendations.length > 0) { + await this.showRecommendation(0, true) + } + } + + private clearRejectionTimer() { + if (this._timer !== undefined) { + clearInterval(this._timer) + this._timer = undefined + } + } + + private sendPerceivedLatencyTelemetry() { + if (vscode.window.activeTextEditor) { + const languageContext = runtimeLanguageContext.getLanguageContext( + vscode.window.activeTextEditor.document.languageId, + vscode.window.activeTextEditor.document.fileName.substring( + vscode.window.activeTextEditor.document.fileName.lastIndexOf('.') + 1 + ) + ) + telemetry.codewhisperer_perceivedLatency.emit({ + codewhispererRequestId: this.requestId, + codewhispererSessionId: session.sessionId, + codewhispererTriggerType: session.triggerType, + codewhispererCompletionType: session.getCompletionType(0), + codewhispererCustomizationArn: getSelectedCustomization().arn, + codewhispererLanguage: languageContext.language, + duration: Date.now() - this.lastInvocationTime, + passive: true, + credentialStartUrl: AuthUtil.instance.startUrl, + result: 'Succeeded', + }) + } + } +} diff --git a/packages/core/src/codewhisperer/service/recommendationService.ts b/packages/core/src/codewhisperer/service/recommendationService.ts new file mode 100644 index 00000000000..de78b435913 --- /dev/null +++ b/packages/core/src/codewhisperer/service/recommendationService.ts @@ -0,0 +1,122 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { ConfigurationEntry, GetRecommendationsResponse } from '../models/model' +import { isInlineCompletionEnabled } from '../util/commonUtil' +import { + CodewhispererAutomatedTriggerType, + CodewhispererTriggerType, + telemetry, +} from '../../shared/telemetry/telemetry' +import { InlineCompletionService } from '../service/inlineCompletionService' +import { ClassifierTrigger } from './classifierTrigger' +import { DefaultCodeWhispererClient } from '../client/codewhisperer' +import { randomUUID } from '../../shared/crypto' +import { TelemetryHelper } from '../util/telemetryHelper' +import { AuthUtil } from '../util/authUtil' + +export interface SuggestionActionEvent { + readonly editor: vscode.TextEditor | undefined + readonly isRunning: boolean + readonly triggerType: CodewhispererTriggerType + readonly response: GetRecommendationsResponse | undefined +} + +export class RecommendationService { + static #instance: RecommendationService + + private _isRunning: boolean = false + get isRunning() { + return this._isRunning + } + + private _onSuggestionActionEvent = new vscode.EventEmitter() + get suggestionActionEvent(): vscode.Event { + return this._onSuggestionActionEvent.event + } + + private _acceptedSuggestionCount: number = 0 + get acceptedSuggestionCount() { + return this._acceptedSuggestionCount + } + + private _totalValidTriggerCount: number = 0 + get totalValidTriggerCount() { + return this._totalValidTriggerCount + } + + public static get instance() { + return (this.#instance ??= new RecommendationService()) + } + + incrementAcceptedCount() { + this._acceptedSuggestionCount++ + } + + incrementValidTriggerCount() { + this._totalValidTriggerCount++ + } + + async generateRecommendation( + client: DefaultCodeWhispererClient, + editor: vscode.TextEditor, + triggerType: CodewhispererTriggerType, + config: ConfigurationEntry, + autoTriggerType?: CodewhispererAutomatedTriggerType, + event?: vscode.TextDocumentChangeEvent + ) { + // TODO: should move all downstream auth check(inlineCompletionService, recommendationHandler etc) to here(upstream) instead of spreading everywhere + if (AuthUtil.instance.isConnected() && AuthUtil.instance.requireProfileSelection()) { + return + } + + if (this._isRunning) { + return + } + + /** + * Use an existing trace ID if invoked through a command (e.g., manual invocation), + * otherwise generate a new trace ID + */ + const traceId = telemetry.attributes?.traceId ?? randomUUID() + TelemetryHelper.instance.setTraceId(traceId) + await telemetry.withTraceId(async () => { + if (isInlineCompletionEnabled()) { + if (triggerType === 'OnDemand') { + ClassifierTrigger.instance.recordClassifierResultForManualTrigger(editor) + } + + this._isRunning = true + let response: GetRecommendationsResponse | undefined = undefined + + try { + this._onSuggestionActionEvent.fire({ + editor: editor, + isRunning: true, + triggerType: triggerType, + response: undefined, + }) + + response = await InlineCompletionService.instance.getPaginatedRecommendation( + client, + editor, + triggerType, + config, + autoTriggerType, + event + ) + } finally { + this._isRunning = false + this._onSuggestionActionEvent.fire({ + editor: editor, + isRunning: false, + triggerType: triggerType, + response: response, + }) + } + } + }, traceId) + } +} diff --git a/packages/core/src/codewhisperer/service/referenceInlineProvider.ts b/packages/core/src/codewhisperer/service/referenceInlineProvider.ts index a90565797fb..6fe0cf122f2 100644 --- a/packages/core/src/codewhisperer/service/referenceInlineProvider.ts +++ b/packages/core/src/codewhisperer/service/referenceInlineProvider.ts @@ -35,7 +35,7 @@ export class ReferenceInlineProvider implements vscode.CodeLensProvider { } public setInlineReference(line: number, suggestion: string, references: References | undefined) { - const startTime = performance.now() + const startTime = Date.now() this.ranges = [] this.refs = [] if ( @@ -53,7 +53,7 @@ export class ReferenceInlineProvider implements vscode.CodeLensProvider { const licenses = [...n].join(', ') this.ranges.push(new vscode.Range(line, 0, line, 1)) this.refs.push(CodeWhispererConstants.suggestionDetailReferenceText(licenses)) - const duration = performance.now() - startTime + const duration = Date.now() - startTime if (duration > 100) { getLogger().warn(`setInlineReference takes ${duration}ms`) } @@ -70,7 +70,7 @@ export class ReferenceInlineProvider implements vscode.CodeLensProvider { document: vscode.TextDocument, token: vscode.CancellationToken ): vscode.CodeLens[] | Thenable { - const startTime = performance.now() + const startTime = Date.now() const codeLenses: vscode.CodeLens[] = [] for (let i = 0; i < this.ranges.length; i++) { const codeLens = new vscode.CodeLens(this.ranges[i]) @@ -82,7 +82,7 @@ export class ReferenceInlineProvider implements vscode.CodeLensProvider { } codeLenses.push(codeLens) } - const duration = performance.now() - startTime + const duration = Date.now() - startTime if (duration > 100) { getLogger().warn(`setInlineReference takes ${duration}ms`) } diff --git a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts index b1f7f73907b..9990b50fd96 100644 --- a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts @@ -189,8 +189,11 @@ export class IssueItem extends vscode.TreeItem { } private getDescription() { + const positionStr = `[Ln ${this.issue.startLine + 1}]` const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() - return groupingStrategy !== CodeIssueGroupingStrategy.FileLocation ? `${path.basename(this.filePath)}` : '' + return groupingStrategy !== CodeIssueGroupingStrategy.FileLocation + ? `${path.basename(this.filePath)} ${positionStr}` + : positionStr } private getContextValue() { diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts index 20ef306f7ab..c2934dc24ba 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts @@ -68,12 +68,29 @@ export function throwIfCancelled() { } export function updateJobHistory() { - if (transformByQState.getJobId() !== '') { + if (transformByQState.getJobId() !== '' && transformByQState.getSourceJDKVersion() !== undefined) { sessionJobHistory[transformByQState.getJobId()] = { startTime: transformByQState.getStartTime(), projectName: transformByQState.getProjectName(), status: transformByQState.getPolledJobStatus(), duration: convertToTimeString(calculateTotalLatency(CodeTransformTelemetryState.instance.getStartTime())), + transformationType: transformByQState.getTransformationType() ?? 'N/A', + sourceJDKVersion: + transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE + ? (transformByQState.getSourceJDKVersion() ?? 'N/A') + : 'N/A', + targetJDKVersion: + transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE + ? (transformByQState.getTargetJDKVersion() ?? 'N/A') + : 'N/A', + customDependencyVersionsFilePath: + transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE + ? transformByQState.getCustomDependencyVersionFilePath() || 'N/A' + : 'N/A', + customBuildCommand: + transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE + ? transformByQState.getCustomBuildCommand() || 'N/A' + : 'N/A', } } return sessionJobHistory @@ -275,7 +292,8 @@ function isExcludedSourceFile(path: string): boolean { return sourceExcludedExtensions.some((extension) => path.endsWith(extension)) } -// zip all dependency files and all source files excluding "target" (contains large JARs) plus ".git" and ".idea" (may appear in diff.patch) +// zip all dependency files and all source files +// excludes "target" (contains large JARs) plus ".git", ".idea", and ".github" (may appear in diff.patch) export function getFilesRecursively(dir: string, isDependenciesFolder: boolean): string[] { const entries = nodefs.readdirSync(dir, { withFileTypes: true }) const files = entries.flatMap((entry) => { @@ -284,7 +302,12 @@ export function getFilesRecursively(dir: string, isDependenciesFolder: boolean): if (isDependenciesFolder) { // include all dependency files return getFilesRecursively(res, isDependenciesFolder) - } else if (entry.name !== 'target' && entry.name !== '.git' && entry.name !== '.idea') { + } else if ( + entry.name !== 'target' && + entry.name !== '.git' && + entry.name !== '.idea' && + entry.name !== '.github' + ) { // exclude the above directories when zipping source code return getFilesRecursively(res, isDependenciesFolder) } else { @@ -743,7 +766,7 @@ export async function pollTransformationJob(jobId: string, validStates: string[] } await sleep(CodeWhispererConstants.transformationJobPollingIntervalSeconds * 1000) } catch (e: any) { - getLogger().error(`CodeTransformation: GetTransformation error = %O`, e) + getLogger().error(`CodeTransformation: error = %O`, e) throw e } } @@ -827,11 +850,11 @@ async function processClientInstructions(jobId: string, clientInstructionsPath: getLogger().info(`CodeTransformation: copied project to ${destinationPath}`) const diffContents = await fs.readFileText(clientInstructionsPath) if (diffContents.trim()) { - const diffModel = new DiffModel() - diffModel.parseDiff(clientInstructionsPath, path.join(destinationPath, 'sources'), true) // show user the diff.patch const doc = await vscode.workspace.openTextDocument(clientInstructionsPath) await vscode.window.showTextDocument(doc, { viewColumn: vscode.ViewColumn.One }) + const diffModel = new DiffModel() + diffModel.parseDiff(clientInstructionsPath, path.join(destinationPath, 'sources'), true) } else { // still need to set the project copy so that we can use it below transformByQState.setProjectCopyFilePath(path.join(destinationPath, 'sources')) diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts index 400acd5fa7a..b16ea64022c 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode' import * as path from 'path' import * as os from 'os' +import * as YAML from 'js-yaml' import xml2js = require('xml2js') import * as CodeWhispererConstants from '../../models/constants' import { existsSync, readFileSync, writeFileSync } from 'fs' // eslint-disable-line no-restricted-imports @@ -119,15 +120,63 @@ export async function parseBuildFile() { return undefined } -// return the first missing key in the custom versions file, or undefined if all required keys are present -export async function validateCustomVersionsFile(fileContents: string) { +// return an error message, or undefined if YAML file is valid +export function validateCustomVersionsFile(fileContents: string) { const requiredKeys = ['dependencyManagement', 'identifier', 'targetVersion', 'originType'] for (const key of requiredKeys) { if (!fileContents.includes(key)) { getLogger().info(`CodeTransformation: .YAML file is missing required key: ${key}`) - return key + return `Missing required key: \`${key}\`` } } + try { + const yaml = YAML.load(fileContents) as any + const dependencies = yaml?.dependencyManagement?.dependencies || [] + const plugins = yaml?.dependencyManagement?.plugins || [] + const dependenciesAndPlugins = dependencies.concat(plugins) + + if (dependenciesAndPlugins.length === 0) { + getLogger().info('CodeTransformation: .YAML file must contain at least dependencies or plugins') + return `YAML file must contain at least \`dependencies\` or \`plugins\` under \`dependencyManagement\`` + } + for (const item of dependencies) { + const errorMessage = validateItem(item, false) + if (errorMessage) { + return errorMessage + } + } + for (const item of plugins) { + const errorMessage = validateItem(item, true) + if (errorMessage) { + return errorMessage + } + } + return undefined + } catch (err: any) { + getLogger().info(`CodeTransformation: Invalid YAML format: ${err.message}`) + return `Invalid YAML format: ${err.message}` + } +} + +// return an error message, or undefined if item is valid +function validateItem(item: any, isPlugin: boolean) { + const validOriginTypes = ['FIRST_PARTY', 'THIRD_PARTY'] + if (!isPlugin && !/^[^\s:]+:[^\s:]+$/.test(item.identifier)) { + getLogger().info(`CodeTransformation: Invalid identifier format: ${item.identifier}`) + return `Invalid dependency identifier format: \`${item.identifier}\`. Must be in format \`groupId:artifactId\` without spaces` + } + if (isPlugin && !item.identifier?.trim()) { + getLogger().info('CodeTransformation: Missing identifier in plugin') + return 'Missing `identifier` in plugin' + } + if (!validOriginTypes.includes(item.originType)) { + getLogger().info(`CodeTransformation: Invalid originType: ${item.originType}`) + return `Invalid originType: \`${item.originType}\`. Must be either \`FIRST_PARTY\` or \`THIRD_PARTY\`` + } + if (!item.targetVersion?.trim()) { + getLogger().info(`CodeTransformation: Missing targetVersion in: ${item.identifier}`) + return `Missing \`targetVersion\` in: \`${item.identifier}\`` + } return undefined } diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts index b38a6ef1da8..22fcd1e6fa1 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts @@ -17,14 +17,14 @@ import globals from '../../../shared/extensionGlobals' function collectDependenciesAndMetadata(dependenciesFolderPath: string, workingDirPath: string) { getLogger().info('CodeTransformation: running mvn clean test-compile with maven JAR') - const baseCommand = transformByQState.getMavenName() - const jarPath = globals.context.asAbsolutePath(path.join('resources', 'amazonQCT', 'QCT-Maven-6-16.jar')) + const baseCommand = transformByQState.getMavenName() // always 'mvn' + const jarPath = globals.context.asAbsolutePath(path.join('resources', 'amazonQCT', 'QCT-Maven-1-0-156-0.jar')) getLogger().info('CodeTransformation: running Maven extension with JAR') const args = [ - `-Dmaven.ext.class.path=${jarPath}`, - `-Dcom.amazon.aws.developer.transform.jobDirectory=${dependenciesFolderPath}`, + `-Dmaven.ext.class.path="${jarPath}"`, + `-Dcom.amazon.aws.developer.transform.jobDirectory="${dependenciesFolderPath}"`, 'clean', 'test-compile', ] diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHistoryHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHistoryHandler.ts index 1876ac059e4..6aba818b4fc 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHistoryHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHistoryHandler.ts @@ -26,6 +26,11 @@ export interface HistoryObject { diffPath: string summaryPath: string jobId: string + transformationType: string + sourceJDKVersion: string + targetJDKVersion: string + customDependencyVersionFilePath: string + customBuildCommand: string } export interface JobMetadata { @@ -71,6 +76,11 @@ export async function readHistoryFile(): Promise { diffPath: jobInfo[4], summaryPath: jobInfo[5], jobId: jobInfo[6], + transformationType: jobInfo[7], + sourceJDKVersion: jobInfo[8], + targetJDKVersion: jobInfo[9], + customDependencyVersionFilePath: jobInfo[10], + customBuildCommand: jobInfo[11], }) } } @@ -125,14 +135,22 @@ export async function writeToHistoryFile( status: string, duration: string, jobId: string, - jobHistoryPath: string + jobHistoryPath: string, + transformationType: string, + sourceJDKVersion: string, + targetJDKVersion: string, + customDependencyVersionFilePath: string, + customBuildCommand: string ) { const historyLogFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') // create transform folder if necessary if (!(await fs.existsFile(historyLogFilePath))) { await fs.mkdir(path.dirname(historyLogFilePath)) // create headers of new transformation history file - await fs.writeFile(historyLogFilePath, 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n') + await fs.writeFile( + historyLogFilePath, + 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\ttransformation_type\tsource_jdk_version\ttarget_jdk_version\tcustom_dependency_version_file_path\tcustom_build_command\n' + ) } const artifactsExist = status === 'COMPLETED' || status === 'PARTIALLY_COMPLETED' const fields = [ @@ -143,6 +161,11 @@ export async function writeToHistoryFile( artifactsExist ? path.join(jobHistoryPath, 'diff.patch') : '', artifactsExist ? path.join(jobHistoryPath, 'summary', 'summary.md') : '', jobId, + transformationType, + sourceJDKVersion, + targetJDKVersion, + customDependencyVersionFilePath, + customBuildCommand, ] const jobDetails = fields.join('\t') + '\n' @@ -318,7 +341,8 @@ async function updateHistoryFile(status: string, duration: string, jobHistoryPat for (const job of jobs) { if (job) { const jobInfo = job.split('\t') - // startTime: jobInfo[0], projectName: jobInfo[1], status: jobInfo[2], duration: jobInfo[3], diffPath: jobInfo[4], summaryPath: jobInfo[5], jobId: jobInfo[6] + // 0: startTime, 1: projectName, 2: status, 3: duration, 4: diffPath, 5: summaryPath, 6: jobId + // 7: transformationType, 8: sourceJDKVersion, 9: targetJDKVersion, 10: customDependencyVersionFilePath, 11: customBuildCommand if (jobInfo[6] === jobId) { // update any values if applicable jobInfo[2] = status @@ -341,7 +365,10 @@ async function updateHistoryFile(status: string, duration: string, jobHistoryPat } // rewrite file - await fs.writeFile(historyLogFilePath, 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n') + await fs.writeFile( + historyLogFilePath, + 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\ttransformation_type\tsource_jdk_version\ttarget_jdk_version\tcustom_dependency_version_file_path\tcustom_build_command\n' + ) const tsvContent = history.map((row) => row.join('\t')).join('\n') + '\n' await fs.appendFile(historyLogFilePath, tsvContent) diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts index 35e8319ab46..97e69570c76 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts @@ -120,6 +120,11 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider diffPath: '', summaryPath: '', jobId: transformByQState.getJobId(), + transformationType: current.transformationType, + sourceJDKVersion: current.sourceJDKVersion, + targetJDKVersion: current.targetJDKVersion, + customDependencyVersionFilePath: current.customDependencyVersionsFilePath, + customBuildCommand: current.customBuildCommand, }) } return ` @@ -208,6 +213,11 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider Summary File Job Id Refresh Job + Transformation Type + Source JDK Version + Target JDK Version + Custom Dependency Version File Path + Custom Build Command @@ -242,6 +252,11 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider ↻ + ${job.transformationType ?? ''} + ${job.sourceJDKVersion ?? ''} + ${job.targetJDKVersion ?? ''} + ${job.customDependencyVersionFilePath ?? ''} + ${job.customBuildCommand ? `mvn ${job.customBuildCommand}` : ''} ` ) diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts index c37ecfca3bb..68d11b800fc 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts @@ -166,6 +166,8 @@ export class DiffModel { throw new Error(CodeWhispererConstants.noChangesMadeMessage) } + getLogger().info(`CodeTransformation: parsing patch file at ${pathToDiff}`) + let changedFiles = parsePatch(diffContents) // exclude dependency_upgrade.yml from patch application changedFiles = changedFiles.filter((file) => !file.oldFileName?.includes('dependency_upgrade')) diff --git a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts new file mode 100644 index 00000000000..0989f022245 --- /dev/null +++ b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts @@ -0,0 +1,319 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../shared/logger/logger' +import * as CodeWhispererConstants from '../models/constants' +import globals from '../../shared/extensionGlobals' +import { vsCodeState } from '../models/model' +import { CodewhispererLanguage, telemetry } from '../../shared/telemetry/telemetry' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { TelemetryHelper } from '../util/telemetryHelper' +import { AuthUtil } from '../util/authUtil' +import { getSelectedCustomization } from '../util/customizationUtil' +import { codeWhispererClient as client } from '../client/codewhisperer' +import { isAwsError } from '../../shared/errors' +import { getUnmodifiedAcceptedTokens } from '../util/commonUtil' + +interface CodeWhispererToken { + range: vscode.Range + text: string + accepted: number +} + +const autoClosingKeystrokeInputs = ['[]', '{}', '()', '""', "''"] + +/** + * This singleton class is mainly used for calculating the code written by codeWhisperer + * TODO: Remove this tracker, uses user written code tracker instead. + * This is kept in codebase for server side backward compatibility until service fully switch to user written code + */ +export class CodeWhispererCodeCoverageTracker { + private _acceptedTokens: { [key: string]: CodeWhispererToken[] } + private _totalTokens: { [key: string]: number } + private _timer?: NodeJS.Timer + private _startTime: number + private _language: CodewhispererLanguage + private _serviceInvocationCount: number + + private constructor(language: CodewhispererLanguage) { + this._acceptedTokens = {} + this._totalTokens = {} + this._startTime = 0 + this._language = language + this._serviceInvocationCount = 0 + } + + public get serviceInvocationCount(): number { + return this._serviceInvocationCount + } + + public get acceptedTokens(): { [key: string]: CodeWhispererToken[] } { + return this._acceptedTokens + } + + public get totalTokens(): { [key: string]: number } { + return this._totalTokens + } + + public isActive(): boolean { + return TelemetryHelper.instance.isTelemetryEnabled() && AuthUtil.instance.isConnected() + } + + public incrementServiceInvocationCount() { + this._serviceInvocationCount += 1 + } + + public flush() { + if (!this.isActive()) { + this._totalTokens = {} + this._acceptedTokens = {} + this.closeTimer() + return + } + try { + this.emitCodeWhispererCodeContribution() + } catch (error) { + getLogger().error(`Encountered ${error} when emitting code contribution metric`) + } + } + + // TODO: Improve the range tracking of the accepted recommendation + // TODO: use the editor of the filename, not the current editor + public updateAcceptedTokensCount(editor: vscode.TextEditor) { + const filename = editor.document.fileName + if (filename in this._acceptedTokens) { + for (let i = 0; i < this._acceptedTokens[filename].length; i++) { + const oldText = this._acceptedTokens[filename][i].text + const newText = editor.document.getText(this._acceptedTokens[filename][i].range) + this._acceptedTokens[filename][i].accepted = getUnmodifiedAcceptedTokens(oldText, newText) + } + } + } + + public emitCodeWhispererCodeContribution() { + let totalTokens = 0 + for (const filename in this._totalTokens) { + totalTokens += this._totalTokens[filename] + } + if (vscode.window.activeTextEditor) { + this.updateAcceptedTokensCount(vscode.window.activeTextEditor) + } + // the accepted characters without counting user modification + let acceptedTokens = 0 + // the accepted characters after calculating user modification + let unmodifiedAcceptedTokens = 0 + for (const filename in this._acceptedTokens) { + for (const v of this._acceptedTokens[filename]) { + if (filename in this._totalTokens && this._totalTokens[filename] >= v.accepted) { + unmodifiedAcceptedTokens += v.accepted + acceptedTokens += v.text.length + } + } + } + const percentCount = ((acceptedTokens / totalTokens) * 100).toFixed(2) + const percentage = Math.round(parseInt(percentCount)) + const selectedCustomization = getSelectedCustomization() + if (this._serviceInvocationCount <= 0) { + getLogger().debug(`Skip emiting code contribution metric`) + return + } + telemetry.codewhisperer_codePercentage.emit({ + codewhispererTotalTokens: totalTokens, + codewhispererLanguage: this._language, + codewhispererAcceptedTokens: unmodifiedAcceptedTokens, + codewhispererSuggestedTokens: acceptedTokens, + codewhispererPercentage: percentage ? percentage : 0, + successCount: this._serviceInvocationCount, + codewhispererCustomizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, + credentialStartUrl: AuthUtil.instance.startUrl, + }) + + client + .sendTelemetryEvent({ + telemetryEvent: { + codeCoverageEvent: { + customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, + programmingLanguage: { + languageName: runtimeLanguageContext.toRuntimeLanguage(this._language), + }, + acceptedCharacterCount: acceptedTokens, + unmodifiedAcceptedCharacterCount: unmodifiedAcceptedTokens, + totalCharacterCount: totalTokens, + timestamp: new Date(Date.now()), + }, + }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, + }) + .then() + .catch((error) => { + let requestId: string | undefined + if (isAwsError(error)) { + requestId = error.requestId + } + + getLogger().debug( + `Failed to sendTelemetryEvent to CodeWhisperer, requestId: ${requestId ?? ''}, message: ${ + error.message + }` + ) + }) + } + + private tryStartTimer() { + if (this._timer !== undefined) { + return + } + const currentDate = new globals.clock.Date() + this._startTime = currentDate.getTime() + this._timer = setTimeout(() => { + try { + const currentTime = new globals.clock.Date().getTime() + const delay: number = CodeWhispererConstants.defaultCheckPeriodMillis + const diffTime: number = this._startTime + delay + if (diffTime <= currentTime) { + let totalTokens = 0 + for (const filename in this._totalTokens) { + totalTokens += this._totalTokens[filename] + } + if (totalTokens > 0) { + this.flush() + } else { + getLogger().debug( + `CodeWhispererCodeCoverageTracker: skipped telemetry due to empty tokens array` + ) + } + } + } catch (e) { + getLogger().verbose(`Exception Thrown from CodeWhispererCodeCoverageTracker: ${e}`) + } finally { + this.resetTracker() + this.closeTimer() + } + }, CodeWhispererConstants.defaultCheckPeriodMillis) + } + + private resetTracker() { + this._totalTokens = {} + this._acceptedTokens = {} + this._startTime = 0 + this._serviceInvocationCount = 0 + } + + private closeTimer() { + if (this._timer !== undefined) { + clearTimeout(this._timer) + this._timer = undefined + } + } + + public addAcceptedTokens(filename: string, token: CodeWhispererToken) { + if (!(filename in this._acceptedTokens)) { + this._acceptedTokens[filename] = [] + } + this._acceptedTokens[filename].push(token) + } + + public addTotalTokens(filename: string, count: number) { + if (!(filename in this._totalTokens)) { + this._totalTokens[filename] = 0 + } + this._totalTokens[filename] += count + if (this._totalTokens[filename] < 0) { + this._totalTokens[filename] = 0 + } + } + + public countAcceptedTokens(range: vscode.Range, text: string, filename: string) { + if (!this.isActive()) { + return + } + // generate accepted recommendation token and stored in collection + this.addAcceptedTokens(filename, { range: range, text: text, accepted: text.length }) + this.addTotalTokens(filename, text.length) + } + + // For below 2 edge cases + // 1. newline character with indentation + // 2. 2 character insertion of closing brackets + public getCharacterCountFromComplexEvent(e: vscode.TextDocumentChangeEvent) { + function countChanges(cond: boolean, text: string): number { + if (!cond) { + return 0 + } + if ((text.startsWith('\n') || text.startsWith('\r\n')) && text.trim().length === 0) { + return 1 + } + if (autoClosingKeystrokeInputs.includes(text)) { + return 2 + } + return 0 + } + if (e.contentChanges.length === 2) { + const text1 = e.contentChanges[0].text + const text2 = e.contentChanges[1].text + const text2Count = countChanges(text1.length === 0, text2) + const text1Count = countChanges(text2.length === 0, text1) + return text2Count > 0 ? text2Count : text1Count + } else if (e.contentChanges.length === 1) { + return countChanges(true, e.contentChanges[0].text) + } + return 0 + } + + public isFromUserKeystroke(e: vscode.TextDocumentChangeEvent) { + return e.contentChanges.length === 1 && e.contentChanges[0].text.length === 1 + } + + public countTotalTokens(e: vscode.TextDocumentChangeEvent) { + // ignore no contentChanges. ignore contentChanges from other plugins (formatters) + // only include contentChanges from user keystroke input(one character input). + // Also ignore deletion events due to a known issue of tracking deleted CodeWhiperer tokens. + if (!runtimeLanguageContext.isLanguageSupported(e.document.languageId) || vsCodeState.isCodeWhispererEditing) { + return + } + // a user keystroke input can be + // 1. content change with 1 character insertion + // 2. newline character with indentation + // 3. 2 character insertion of closing brackets + if (this.isFromUserKeystroke(e)) { + this.tryStartTimer() + this.addTotalTokens(e.document.fileName, 1) + } else if (this.getCharacterCountFromComplexEvent(e) !== 0) { + this.tryStartTimer() + const characterIncrease = this.getCharacterCountFromComplexEvent(e) + this.addTotalTokens(e.document.fileName, characterIncrease) + } + // also include multi character input within 50 characters (not from CWSPR) + else if ( + e.contentChanges.length === 1 && + e.contentChanges[0].text.length > 1 && + TelemetryHelper.instance.lastSuggestionInDisplay !== e.contentChanges[0].text + ) { + const multiCharInputSize = e.contentChanges[0].text.length + + // select 50 as the cut-off threshold for counting user input. + // ignore all white space multi char input, this usually comes from reformat. + if (multiCharInputSize < 50 && e.contentChanges[0].text.trim().length > 0) { + this.addTotalTokens(e.document.fileName, multiCharInputSize) + } + } + } + + public static readonly instances = new Map() + + public static getTracker(language: string): CodeWhispererCodeCoverageTracker | undefined { + if (!runtimeLanguageContext.isLanguageSupported(language)) { + return undefined + } + const cwsprLanguage = runtimeLanguageContext.normalizeLanguage(language) + if (!cwsprLanguage) { + return undefined + } + const instance = this.instances.get(cwsprLanguage) ?? new this(cwsprLanguage) + this.instances.set(cwsprLanguage, instance) + return instance + } +} diff --git a/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts b/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts index 32de471878d..7dfb14b5745 100644 --- a/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts +++ b/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts @@ -53,7 +53,7 @@ export class UserWrittenCodeTracker { // for all Q features public onQFeatureInvoked() { this._qUsageCount += 1 - this._lastQInvocationTime = performance.now() + this._lastQInvocationTime = Date.now() } public onQStartsMakingEdits() { @@ -129,10 +129,10 @@ export class UserWrittenCodeTracker { this.reset() return } - const startTime = performance.now() + const startTime = Date.now() this._timer = setTimeout(() => { try { - const currentTime = performance.now() + const currentTime = Date.now() const delay: number = UserWrittenCodeTracker.defaultCheckPeriodMillis const diffTime: number = startTime + delay if (diffTime <= currentTime) { @@ -169,7 +169,7 @@ export class UserWrittenCodeTracker { // due to unhandled edge cases or early terminated code paths // reset it back to false after a reasonable period of time if (this._qIsMakingEdits) { - if (performance.now() - this._lastQInvocationTime > UserWrittenCodeTracker.resetQIsEditingTimeoutMs) { + if (Date.now() - this._lastQInvocationTime > UserWrittenCodeTracker.resetQIsEditingTimeoutMs) { getLogger().warn(`Reset Q is editing state to false.`) this._qIsMakingEdits = false } diff --git a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts index 28ed3952494..c1934ec6a73 100644 --- a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts +++ b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts @@ -21,7 +21,7 @@ import { selectRegionProfileCommand, } from '../commands/basicCommands' import { CodeWhispererCommandDeclarations } from '../commands/gettingStartedPageCommands' -import { CodeScansState, codeScanState, RegionProfile } from '../models/model' +import { CodeScansState, RegionProfile } from '../models/model' import { getNewCustomizationsAvailable, getSelectedCustomization } from '../util/customizationUtil' import { cwQuickPickSource } from '../commands/types' import { AuthUtil } from '../util/authUtil' @@ -70,25 +70,6 @@ export function createOpenReferenceLog(): DataQuickPickItem<'openReferenceLog'> } as DataQuickPickItem<'openReferenceLog'> } -export function createSecurityScan(): DataQuickPickItem<'securityScan'> { - const label = `Full project scan is now /review!` - const icon = codeScanState.getIconForButton() - const description = 'Open in Chat Panel' - - return { - data: 'securityScan', - label: codicon`${icon} ${label}`, - description: description, - onClick: () => - vscode.commands.executeCommand( - 'aws.amazonq.security.scan-statusbar', - placeholder, - 'cwQuickPickSource', - true - ), - } as DataQuickPickItem<'securityScan'> -} - export function createReconnect(): DataQuickPickItem<'reconnect'> { const label = localize('aws.amazonq.reconnectNode.label', 'Re-authenticate to connect') const icon = addColor(getIcon('vscode-debug-disconnect'), 'notificationsErrorIcon.foreground') diff --git a/packages/core/src/codewhisperer/ui/statusBarMenu.ts b/packages/core/src/codewhisperer/ui/statusBarMenu.ts index 46f47e35a2c..345ae641a78 100644 --- a/packages/core/src/codewhisperer/ui/statusBarMenu.ts +++ b/packages/core/src/codewhisperer/ui/statusBarMenu.ts @@ -21,7 +21,6 @@ import { createAutoScans, createSignIn, switchToAmazonQNode, - createSecurityScan, createSelectRegionProfileNode, } from './codeWhispererNodes' import { hasVendedIamCredentials, hasVendedCredentialsFromMetadata } from '../../auth/auth' @@ -52,12 +51,7 @@ function getAmazonQCodeWhispererNodes() { if (hasVendedIamCredentials()) { return [createFreeTierLimitMet(), createOpenReferenceLog()] } - return [ - createFreeTierLimitMet(), - createOpenReferenceLog(), - createSeparator('Other Features'), - createSecurityScan(), - ] + return [createFreeTierLimitMet(), createOpenReferenceLog(), createSeparator('Other Features')] } if (hasVendedIamCredentials()) { @@ -74,7 +68,6 @@ function getAmazonQCodeWhispererNodes() { // Security scans createSeparator('Code Reviews'), ...(AuthUtil.instance.isBuilderIdInUse() ? [] : [createAutoScans(autoScansEnabled)]), - createSecurityScan(), // Amazon Q + others createSeparator('Other Features'), diff --git a/packages/core/src/codewhisperer/util/codeWhispererSession.ts b/packages/core/src/codewhisperer/util/codeWhispererSession.ts index 17d9c998112..4a529941004 100644 --- a/packages/core/src/codewhisperer/util/codeWhispererSession.ts +++ b/packages/core/src/codewhisperer/util/codeWhispererSession.ts @@ -53,13 +53,13 @@ class CodeWhispererSession { setFetchCredentialStart() { if (this.fetchCredentialStartTime === 0 && this.invokeSuggestionStartTime !== 0) { - this.fetchCredentialStartTime = performance.now() + this.fetchCredentialStartTime = Date.now() } } setSdkApiCallStart() { if (this.sdkApiCallStartTime === 0 && this.fetchCredentialStartTime !== 0) { - this.sdkApiCallStartTime = performance.now() + this.sdkApiCallStartTime = Date.now() } } diff --git a/packages/core/src/codewhisperer/util/commonUtil.ts b/packages/core/src/codewhisperer/util/commonUtil.ts index 729d3b7ed12..d2df78f1369 100644 --- a/packages/core/src/codewhisperer/util/commonUtil.ts +++ b/packages/core/src/codewhisperer/util/commonUtil.ts @@ -3,18 +3,80 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as vscode from 'vscode' +import * as semver from 'semver' import { distance } from 'fastest-levenshtein' import { getInlineSuggestEnabled } from '../../shared/utilities/editorUtilities' +import { + AWSTemplateCaseInsensitiveKeyWords, + AWSTemplateKeyWords, + JsonConfigFileNamingConvention, +} from '../models/constants' export function getLocalDatetime() { const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone return new Date().toLocaleString([], { timeZone: timezone }) } +export function asyncCallWithTimeout(asyncPromise: Promise, message: string, timeLimit: number): Promise { + let timeoutHandle: NodeJS.Timeout + const timeoutPromise = new Promise((_resolve, reject) => { + timeoutHandle = setTimeout(() => reject(new Error(message)), timeLimit) + }) + return Promise.race([asyncPromise, timeoutPromise]).then((result) => { + clearTimeout(timeoutHandle) + return result as T + }) +} + export function isInlineCompletionEnabled() { return getInlineSuggestEnabled() } +// This is the VS Code version that started to have regressions in inline completion API +export function isVscHavingRegressionInlineCompletionApi() { + return semver.gte(vscode.version, '1.78.0') && getInlineSuggestEnabled() +} + +export function getFileExt(languageId: string) { + switch (languageId) { + case 'java': + return '.java' + case 'python': + return '.py' + default: + break + } + return undefined +} + +/** + * Returns the longest overlap between the Suffix of firstString and Prefix of second string + * getPrefixSuffixOverlap("adwg31", "31ggrs") = "31" + */ +export function getPrefixSuffixOverlap(firstString: string, secondString: string) { + let i = Math.min(firstString.length, secondString.length) + while (i > 0) { + if (secondString.slice(0, i) === firstString.slice(-i)) { + break + } + i-- + } + return secondString.slice(0, i) +} + +export function checkLeftContextKeywordsForJson(fileName: string, leftFileContent: string, language: string): boolean { + if ( + language === 'json' && + !AWSTemplateKeyWords.some((substring) => leftFileContent.includes(substring)) && + !AWSTemplateCaseInsensitiveKeyWords.some((substring) => leftFileContent.toLowerCase().includes(substring)) && + !JsonConfigFileNamingConvention.has(fileName.toLowerCase()) + ) { + return true + } + return false +} + // With edit distance, complicate usermodification can be considered as simple edit(add, delete, replace), // and thus the unmodified part of recommendation length can be deducted/approximated // ex. (modified > original): originalRecom: foo -> modifiedRecom: fobarbarbaro, distance = 9, delta = 12 - 9 = 3 diff --git a/packages/core/src/codewhisperer/util/editorContext.ts b/packages/core/src/codewhisperer/util/editorContext.ts new file mode 100644 index 00000000000..9ea3020dba4 --- /dev/null +++ b/packages/core/src/codewhisperer/util/editorContext.ts @@ -0,0 +1,427 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as codewhispererClient from '../client/codewhisperer' +import * as path from 'path' +import * as CodeWhispererConstants from '../models/constants' +import { getTabSizeSetting } from '../../shared/utilities/editorUtilities' +import { truncate } from '../../shared/utilities/textUtilities' +import { getLogger } from '../../shared/logger/logger' +import { runtimeLanguageContext } from './runtimeLanguageContext' +import { fetchSupplementalContext } from './supplementalContext/supplementalContextUtil' +import { editorStateMaxLength, supplementalContextTimeoutInMs } from '../models/constants' +import { getSelectedCustomization } from './customizationUtil' +import { selectFrom } from '../../shared/utilities/tsUtils' +import { checkLeftContextKeywordsForJson } from './commonUtil' +import { CodeWhispererSupplementalContext } from '../models/model' +import { getOptOutPreference } from '../../shared/telemetry/util' +import { indent } from '../../shared/utilities/textUtilities' +import { isInDirectory } from '../../shared/filesystemUtilities' +import { AuthUtil } from './authUtil' +import { predictionTracker } from '../nextEditPrediction/activation' +import { BaseLanguageClient } from 'vscode-languageclient' + +let tabSize: number = getTabSizeSetting() + +function getEnclosingNotebook(editor: vscode.TextEditor): vscode.NotebookDocument | undefined { + // For notebook cells, find the existing notebook with a cell that matches the current editor. + return vscode.workspace.notebookDocuments.find( + (nb) => + nb.notebookType === 'jupyter-notebook' && nb.getCells().some((cell) => cell.document === editor.document) + ) +} + +export function getNotebookContext( + notebook: vscode.NotebookDocument, + editor: vscode.TextEditor, + languageName: string, + caretLeftFileContext: string, + caretRightFileContext: string +) { + // Expand the context for a cell inside of a noteboo with whatever text fits from the preceding and subsequent cells + const allCells = notebook.getCells() + const cellIndex = allCells.findIndex((cell) => cell.document === editor.document) + // Extract text from prior cells if there is enough room in left file context + if (caretLeftFileContext.length < CodeWhispererConstants.charactersLimit - 1) { + const leftCellsText = getNotebookCellsSliceContext( + allCells.slice(0, cellIndex), + CodeWhispererConstants.charactersLimit - (caretLeftFileContext.length + 1), + languageName, + true + ) + if (leftCellsText.length > 0) { + caretLeftFileContext = addNewlineIfMissing(leftCellsText) + caretLeftFileContext + } + } + // Extract text from subsequent cells if there is enough room in right file context + if (caretRightFileContext.length < CodeWhispererConstants.charactersLimit - 1) { + const rightCellsText = getNotebookCellsSliceContext( + allCells.slice(cellIndex + 1), + CodeWhispererConstants.charactersLimit - (caretRightFileContext.length + 1), + languageName, + false + ) + if (rightCellsText.length > 0) { + caretRightFileContext = addNewlineIfMissing(caretRightFileContext) + rightCellsText + } + } + return { caretLeftFileContext, caretRightFileContext } +} + +export function getNotebookCellContext(cell: vscode.NotebookCell, referenceLanguage?: string): string { + // Extract the text verbatim if the cell is code and the cell has the same language. + // Otherwise, add the correct comment string for the reference language + const cellText = cell.document.getText() + if ( + cell.kind === vscode.NotebookCellKind.Markup || + (runtimeLanguageContext.normalizeLanguage(cell.document.languageId) ?? cell.document.languageId) !== + referenceLanguage + ) { + const commentPrefix = runtimeLanguageContext.getSingleLineCommentPrefix(referenceLanguage) + if (commentPrefix === '') { + return cellText + } + return cell.document + .getText() + .split('\n') + .map((line) => `${commentPrefix}${line}`) + .join('\n') + } + return cellText +} + +export function getNotebookCellsSliceContext( + cells: vscode.NotebookCell[], + maxLength: number, + referenceLanguage: string, + fromStart: boolean +): string { + // Extract context from array of notebook cells that fits inside `maxLength` characters, + // from either the start or the end of the array. + let output: string[] = [] + if (!fromStart) { + cells = cells.reverse() + } + cells.some((cell) => { + const cellText = addNewlineIfMissing(getNotebookCellContext(cell, referenceLanguage)) + if (cellText.length > 0) { + if (cellText.length >= maxLength) { + if (fromStart) { + output.push(cellText.substring(0, maxLength)) + } else { + output.push(cellText.substring(cellText.length - maxLength)) + } + return true + } + output.push(cellText) + maxLength -= cellText.length + } + }) + if (!fromStart) { + output = output.reverse() + } + return output.join('') +} + +export function addNewlineIfMissing(text: string): string { + if (text.length > 0 && !text.endsWith('\n')) { + text += '\n' + } + return text +} + +export function extractContextForCodeWhisperer(editor: vscode.TextEditor): codewhispererClient.FileContext { + const document = editor.document + const curPos = editor.selection.active + const offset = document.offsetAt(curPos) + + let caretLeftFileContext = editor.document.getText( + new vscode.Range( + document.positionAt(offset - CodeWhispererConstants.charactersLimit), + document.positionAt(offset) + ) + ) + let caretRightFileContext = editor.document.getText( + new vscode.Range( + document.positionAt(offset), + document.positionAt(offset + CodeWhispererConstants.charactersLimit) + ) + ) + let languageName = 'plaintext' + if (!checkLeftContextKeywordsForJson(document.fileName, caretLeftFileContext, editor.document.languageId)) { + languageName = runtimeLanguageContext.resolveLang(editor.document) + } + if (editor.document.uri.scheme === 'vscode-notebook-cell') { + const notebook = getEnclosingNotebook(editor) + if (notebook) { + ;({ caretLeftFileContext, caretRightFileContext } = getNotebookContext( + notebook, + editor, + languageName, + caretLeftFileContext, + caretRightFileContext + )) + } + } + + return { + fileUri: editor.document.uri.toString().substring(0, CodeWhispererConstants.filenameCharsLimit), + filename: getFileRelativePath(editor), + programmingLanguage: { + languageName: languageName, + }, + leftFileContent: caretLeftFileContext, + rightFileContent: caretRightFileContext, + } as codewhispererClient.FileContext +} + +export function getFileName(editor: vscode.TextEditor): string { + const fileName = path.basename(editor.document.fileName) + return fileName.substring(0, CodeWhispererConstants.filenameCharsLimit) +} + +export function getFileRelativePath(editor: vscode.TextEditor): string { + const fileName = path.basename(editor.document.fileName) + let relativePath = '' + const workspaceFolder = vscode.workspace.getWorkspaceFolder(editor.document.uri) + if (!workspaceFolder) { + relativePath = fileName + } else { + const workspacePath = workspaceFolder.uri.fsPath + const filePath = editor.document.uri.fsPath + relativePath = path.relative(workspacePath, filePath) + } + // For notebook files, we want to use the programming language for each cell for the code suggestions, so change + // the filename sent in the request to reflect that language + if (relativePath.endsWith('.ipynb')) { + const fileExtension = runtimeLanguageContext.getLanguageExtensionForNotebook(editor.document.languageId) + if (fileExtension !== undefined) { + const filenameWithNewExtension = relativePath.substring(0, relativePath.length - 5) + fileExtension + return filenameWithNewExtension.substring(0, CodeWhispererConstants.filenameCharsLimit) + } + } + return relativePath.substring(0, CodeWhispererConstants.filenameCharsLimit) +} + +async function getWorkspaceId(editor: vscode.TextEditor): Promise { + try { + const workspaceIds: { workspaces: { workspaceRoot: string; workspaceId: string }[] } = + await vscode.commands.executeCommand('aws.amazonq.getWorkspaceId') + for (const item of workspaceIds.workspaces) { + const path = vscode.Uri.parse(item.workspaceRoot).fsPath + if (isInDirectory(path, editor.document.uri.fsPath)) { + return item.workspaceId + } + } + } catch (err) { + getLogger().warn(`No workspace id found ${err}`) + } + return undefined +} + +export async function buildListRecommendationRequest( + editor: vscode.TextEditor, + nextToken: string, + allowCodeWithReference: boolean, + languageClient?: BaseLanguageClient +): Promise<{ + request: codewhispererClient.ListRecommendationsRequest + supplementalMetadata: CodeWhispererSupplementalContext | undefined +}> { + const fileContext = extractContextForCodeWhisperer(editor) + + const tokenSource = new vscode.CancellationTokenSource() + setTimeout(() => { + tokenSource.cancel() + }, supplementalContextTimeoutInMs) + + const supplementalContexts = await fetchSupplementalContext(editor, tokenSource.token, languageClient) + + logSupplementalContext(supplementalContexts) + + // Get predictionSupplementalContext from PredictionTracker + let predictionSupplementalContext: codewhispererClient.SupplementalContext[] = [] + if (predictionTracker) { + predictionSupplementalContext = await predictionTracker.generatePredictionSupplementalContext() + } + + const selectedCustomization = getSelectedCustomization() + const completionSupplementalContext: codewhispererClient.SupplementalContext[] = supplementalContexts + ? supplementalContexts.supplementalContextItems.map((v) => { + return selectFrom(v, 'content', 'filePath') + }) + : [] + + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile + + const editorState = getEditorState(editor, fileContext) + + // Combine inline and prediction supplemental contexts + const finalSupplementalContext = completionSupplementalContext.concat(predictionSupplementalContext) + return { + request: { + fileContext: fileContext, + nextToken: nextToken, + referenceTrackerConfiguration: { + recommendationsWithReferences: allowCodeWithReference ? 'ALLOW' : 'BLOCK', + }, + supplementalContexts: finalSupplementalContext, + editorState: editorState, + maxResults: CodeWhispererConstants.maxRecommendations, + customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, + optOutPreference: getOptOutPreference(), + workspaceId: await getWorkspaceId(editor), + profileArn: profile?.arn, + }, + supplementalMetadata: supplementalContexts, + } +} + +export async function buildGenerateRecommendationRequest(editor: vscode.TextEditor): Promise<{ + request: codewhispererClient.GenerateRecommendationsRequest + supplementalMetadata: CodeWhispererSupplementalContext | undefined +}> { + const fileContext = extractContextForCodeWhisperer(editor) + + const tokenSource = new vscode.CancellationTokenSource() + // the supplement context fetch mechanisms each has a timeout of supplementalContextTimeoutInMs + // adding 10 ms for overall timeout as buffer + setTimeout(() => { + tokenSource.cancel() + }, supplementalContextTimeoutInMs + 10) + const supplementalContexts = await fetchSupplementalContext(editor, tokenSource.token) + + logSupplementalContext(supplementalContexts) + + return { + request: { + fileContext: fileContext, + maxResults: CodeWhispererConstants.maxRecommendations, + supplementalContexts: supplementalContexts?.supplementalContextItems ?? [], + }, + supplementalMetadata: supplementalContexts, + } +} + +export function validateRequest( + req: codewhispererClient.ListRecommendationsRequest | codewhispererClient.GenerateRecommendationsRequest +): boolean { + const isLanguageNameValid = + req.fileContext.programmingLanguage.languageName !== undefined && + req.fileContext.programmingLanguage.languageName.length >= 1 && + req.fileContext.programmingLanguage.languageName.length <= 128 && + (runtimeLanguageContext.isLanguageSupported(req.fileContext.programmingLanguage.languageName) || + runtimeLanguageContext.isFileFormatSupported( + req.fileContext.filename.substring(req.fileContext.filename.lastIndexOf('.') + 1) + )) + const isFileNameValid = !(req.fileContext.filename === undefined || req.fileContext.filename.length < 1) + const isFileContextValid = !( + req.fileContext.leftFileContent.length > CodeWhispererConstants.charactersLimit || + req.fileContext.rightFileContent.length > CodeWhispererConstants.charactersLimit + ) + if (isFileNameValid && isLanguageNameValid && isFileContextValid) { + return true + } + return false +} + +export function updateTabSize(val: number): void { + tabSize = val +} + +export function getTabSize(): number { + return tabSize +} + +export function getEditorState(editor: vscode.TextEditor, fileContext: codewhispererClient.FileContext): any { + try { + const cursorPosition = editor.selection.active + const cursorOffset = editor.document.offsetAt(cursorPosition) + const documentText = editor.document.getText() + + // Truncate if document content is too large (defined in constants.ts) + let fileText = documentText + if (documentText.length > editorStateMaxLength) { + const halfLength = Math.floor(editorStateMaxLength / 2) + + // Use truncate function to get the text around the cursor position + const leftPart = truncate(documentText.substring(0, cursorOffset), -halfLength, '') + const rightPart = truncate(documentText.substring(cursorOffset), halfLength, '') + + fileText = leftPart + rightPart + } + + return { + document: { + programmingLanguage: { + languageName: fileContext.programmingLanguage.languageName, + }, + relativeFilePath: fileContext.filename, + text: fileText, + }, + cursorState: { + position: { + line: editor.selection.active.line, + character: editor.selection.active.character, + }, + }, + } + } catch (error) { + getLogger().error(`Error generating editor state: ${error}`) + return undefined + } +} + +export function getLeftContext(editor: vscode.TextEditor, line: number): string { + let lineText = '' + try { + if (editor && editor.document.lineAt(line)) { + lineText = editor.document.lineAt(line).text + if (lineText.length > CodeWhispererConstants.contextPreviewLen) { + lineText = + '...' + + lineText.substring( + lineText.length - CodeWhispererConstants.contextPreviewLen - 1, + lineText.length - 1 + ) + } + } + } catch (error) { + getLogger().error(`Error when getting left context ${error}`) + } + + return lineText +} + +function logSupplementalContext(supplementalContext: CodeWhispererSupplementalContext | undefined) { + if (!supplementalContext) { + return + } + + let logString = indent( + `CodeWhispererSupplementalContext: + isUtg: ${supplementalContext.isUtg}, + isProcessTimeout: ${supplementalContext.isProcessTimeout}, + contentsLength: ${supplementalContext.contentsLength}, + latency: ${supplementalContext.latency} + strategy: ${supplementalContext.strategy}`, + 4, + true + ).trimStart() + + for (const [index, context] of supplementalContext.supplementalContextItems.entries()) { + logString += indent(`\nChunk ${index}:\n`, 4, true) + logString += indent( + `Path: ${context.filePath} + Length: ${context.content.length} + Score: ${context.score}`, + 8, + true + ) + } + + getLogger().debug(logString) +} diff --git a/packages/core/src/codewhisperer/util/globalStateUtil.ts b/packages/core/src/codewhisperer/util/globalStateUtil.ts new file mode 100644 index 00000000000..55376a83546 --- /dev/null +++ b/packages/core/src/codewhisperer/util/globalStateUtil.ts @@ -0,0 +1,23 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { vsCodeState } from '../models/model' + +export function resetIntelliSenseState( + isManualTriggerEnabled: boolean, + isAutomatedTriggerEnabled: boolean, + hasResponse: boolean +) { + /** + * Skip when CodeWhisperer service is turned off + */ + if (!isManualTriggerEnabled && !isAutomatedTriggerEnabled) { + return + } + + if (vsCodeState.isIntelliSenseActive && hasResponse) { + vsCodeState.isIntelliSenseActive = false + } +} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts new file mode 100644 index 00000000000..c73a2eebaa4 --- /dev/null +++ b/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts @@ -0,0 +1,130 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import path = require('path') +import { normalize } from '../../../shared/utilities/pathUtils' + +// TODO: functionExtractionPattern, classExtractionPattern, imposrtStatementRegex are not scalable and we will deprecate and remove the usage in the near future +export interface utgLanguageConfig { + extension: string + testFilenamePattern: RegExp[] + functionExtractionPattern?: RegExp + classExtractionPattern?: RegExp + importStatementRegExp?: RegExp +} + +export const utgLanguageConfigs: Record = { + // Java regexes are not working efficiently for class or function extraction + java: { + extension: '.java', + testFilenamePattern: [/^(.+)Test(\.java)$/, /(.+)Tests(\.java)$/, /Test(.+)(\.java)$/], + functionExtractionPattern: + /(?:(?:public|private|protected)\s+)(?:static\s+)?(?:[\w<>]+\s+)?(\w+)\s*\([^)]*\)\s*(?:(?:throws\s+\w+)?\s*)[{;]/gm, // TODO: Doesn't work for generice T functions. + classExtractionPattern: /(?<=^|\n)\s*public\s+class\s+(\w+)/gm, // TODO: Verify these. + importStatementRegExp: /import .*\.([a-zA-Z0-9]+);/, + }, + python: { + extension: '.py', + testFilenamePattern: [/^test_(.+)(\.py)$/, /^(.+)_test(\.py)$/], + functionExtractionPattern: /def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, // Worked fine + classExtractionPattern: /^class\s+(\w+)\s*:/gm, + importStatementRegExp: /from (.*) import.*/, + }, + typescript: { + extension: '.ts', + testFilenamePattern: [/^(.+)\.test(\.ts|\.tsx)$/, /^(.+)\.spec(\.ts|\.tsx)$/], + }, + javascript: { + extension: '.js', + testFilenamePattern: [/^(.+)\.test(\.js|\.jsx)$/, /^(.+)\.spec(\.js|\.jsx)$/], + }, + typescriptreact: { + extension: '.tsx', + testFilenamePattern: [/^(.+)\.test(\.ts|\.tsx)$/, /^(.+)\.spec(\.ts|\.tsx)$/], + }, + javascriptreact: { + extension: '.jsx', + testFilenamePattern: [/^(.+)\.test(\.js|\.jsx)$/, /^(.+)\.spec(\.js|\.jsx)$/], + }, +} + +export function extractFunctions(fileContent: string, regex?: RegExp) { + if (!regex) { + return [] + } + const functionNames: string[] = [] + let match: RegExpExecArray | null + + while ((match = regex.exec(fileContent)) !== null) { + functionNames.push(match[1]) + } + return functionNames +} + +export function extractClasses(fileContent: string, regex?: RegExp) { + if (!regex) { + return [] + } + const classNames: string[] = [] + let match: RegExpExecArray | null + + while ((match = regex.exec(fileContent)) !== null) { + classNames.push(match[1]) + } + return classNames +} + +export function countSubstringMatches(arr1: string[], arr2: string[]): number { + let count = 0 + for (const str1 of arr1) { + for (const str2 of arr2) { + if (str2.toLowerCase().includes(str1.toLowerCase())) { + count++ + } + } + } + return count +} + +export async function isTestFile( + filePath: string, + languageConfig: { + languageId: vscode.TextDocument['languageId'] + fileContent?: string + } +): Promise { + const normalizedFilePath = normalize(filePath) + const pathContainsTest = + normalizedFilePath.includes('tests/') || + normalizedFilePath.includes('test/') || + normalizedFilePath.includes('tst/') + const fileNameMatchTestPatterns = isTestFileByName(normalizedFilePath, languageConfig.languageId) + + if (pathContainsTest || fileNameMatchTestPatterns) { + return true + } + + return false +} + +function isTestFileByName(filePath: string, language: vscode.TextDocument['languageId']): boolean { + const languageConfig = utgLanguageConfigs[language] + if (!languageConfig) { + // We have enabled the support only for python and Java for this check + // as we depend on Regex for this validation. + return false + } + const testFilenamePattern = languageConfig.testFilenamePattern + + const filename = path.basename(filePath) + for (const pattern of testFilenamePattern) { + if (pattern.test(filename)) { + return true + } + } + + return false +} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts new file mode 100644 index 00000000000..aa732a5d70a --- /dev/null +++ b/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts @@ -0,0 +1,400 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import path = require('path') +import { BM25Document, BM25Okapi } from './rankBm25' +import { + crossFileContextConfig, + supplementalContextTimeoutInMs, + supplementalContextMaxTotalLength, +} from '../../models/constants' +import { isTestFile } from './codeParsingUtil' +import { getFileDistance } from '../../../shared/filesystemUtilities' +import { getOpenFilesInWindow } from '../../../shared/utilities/editorUtilities' +import { getLogger } from '../../../shared/logger/logger' +import { + CodeWhispererSupplementalContext, + CodeWhispererSupplementalContextItem, + SupplementalContextStrategy, +} from '../../models/model' +import { waitUntil } from '../../../shared/utilities/timeoutUtils' +import { FeatureConfigProvider } from '../../../shared/featureConfig' +import fs from '../../../shared/fs/fs' +import { BaseLanguageClient } from 'vscode-languageclient' + +import { GetSupplementalContextParams, getSupplementalContextRequestType } from '@aws/language-server-runtimes/protocol' +type CrossFileSupportedLanguage = + | 'java' + | 'python' + | 'javascript' + | 'typescript' + | 'javascriptreact' + | 'typescriptreact' + +// TODO: ugly, can we make it prettier? like we have to manually type 'java', 'javascriptreact' which is error prone +// TODO: Move to another config file or constants file +// Supported language to its corresponding file ext +const supportedLanguageToDialects: Readonly>> = { + java: new Set(['.java']), + python: new Set(['.py']), + javascript: new Set(['.js', '.jsx']), + javascriptreact: new Set(['.js', '.jsx']), + typescript: new Set(['.ts', '.tsx']), + typescriptreact: new Set(['.ts', '.tsx']), +} + +function isCrossFileSupported(languageId: string): languageId is CrossFileSupportedLanguage { + return Object.keys(supportedLanguageToDialects).includes(languageId) +} + +interface Chunk { + fileName: string + content: string + nextContent: string + score?: number +} + +/** + * `none`: supplementalContext is not supported + * `opentabs`: opentabs_BM25 + * `codemap`: repomap + opentabs BM25 + * `bm25`: global_BM25 + * `default`: repomap + global_BM25 + */ +type SupplementalContextConfig = 'none' | 'opentabs' | 'codemap' | 'bm25' | 'default' + +export async function fetchSupplementalContextForSrc( + editor: vscode.TextEditor, + cancellationToken: vscode.CancellationToken, + languageClient?: BaseLanguageClient +): Promise | undefined> { + const supplementalContextConfig = getSupplementalContextConfig(editor.document.languageId) + + // not supported case + if (supplementalContextConfig === 'none') { + return undefined + } + + // fallback to opentabs if projectContext timeout + const opentabsContextPromise = waitUntil( + async function () { + return await fetchOpentabsContext(editor, cancellationToken) + }, + { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } + ) + + // opentabs context will use bm25 and users' open tabs to fetch supplemental context + if (supplementalContextConfig === 'opentabs') { + const supContext = (await opentabsContextPromise) ?? [] + return { + supplementalContextItems: supContext, + strategy: supContext.length === 0 ? 'empty' : 'opentabs', + } + } + + // codemap will use opentabs context plus repomap if it's present + if (supplementalContextConfig === 'codemap') { + let strategy: SupplementalContextStrategy = 'empty' + let hasCodemap: boolean = false + let hasOpentabs: boolean = false + const opentabsContextAndCodemap = await waitUntil( + async function () { + const result: CodeWhispererSupplementalContextItem[] = [] + const opentabsContext = await fetchOpentabsContext(editor, cancellationToken) + const codemap = await fetchProjectContext(editor, 'codemap', languageClient) + + function addToResult(items: CodeWhispererSupplementalContextItem[]) { + for (const item of items) { + const curLen = result.reduce((acc, i) => acc + i.content.length, 0) + if (curLen + item.content.length < supplementalContextMaxTotalLength) { + result.push(item) + } + } + } + + if (codemap && codemap.length > 0) { + addToResult(codemap) + hasCodemap = true + } + + if (opentabsContext && opentabsContext.length > 0) { + addToResult(opentabsContext) + hasOpentabs = true + } + + return result + }, + { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } + ) + + if (hasCodemap) { + strategy = 'codemap' + } else if (hasOpentabs) { + strategy = 'opentabs' + } else { + strategy = 'empty' + } + + return { + supplementalContextItems: opentabsContextAndCodemap ?? [], + strategy: strategy, + } + } + + // global bm25 without repomap + if (supplementalContextConfig === 'bm25') { + const projectBM25Promise = waitUntil( + async function () { + return await fetchProjectContext(editor, 'bm25', languageClient) + }, + { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } + ) + + const [projectContext, opentabsContext] = await Promise.all([projectBM25Promise, opentabsContextPromise]) + if (projectContext && projectContext.length > 0) { + return { + supplementalContextItems: projectContext, + strategy: 'bm25', + } + } + + const supContext = opentabsContext ?? [] + return { + supplementalContextItems: supContext, + strategy: supContext.length === 0 ? 'empty' : 'opentabs', + } + } + + // global bm25 with repomap + const projectContextAndCodemapPromise = waitUntil( + async function () { + return await fetchProjectContext(editor, 'default', languageClient) + }, + { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } + ) + + const [projectContext, opentabsContext] = await Promise.all([ + projectContextAndCodemapPromise, + opentabsContextPromise, + ]) + if (projectContext && projectContext.length > 0) { + return { + supplementalContextItems: projectContext, + strategy: 'default', + } + } + + return { + supplementalContextItems: opentabsContext ?? [], + strategy: 'opentabs', + } +} + +export async function fetchProjectContext( + editor: vscode.TextEditor, + target: 'default' | 'codemap' | 'bm25', + languageclient?: BaseLanguageClient +): Promise { + try { + if (languageclient) { + const request: GetSupplementalContextParams = { + filePath: editor.document.uri.fsPath, + } + const response = await languageclient.sendRequest(getSupplementalContextRequestType.method, request) + return response as CodeWhispererSupplementalContextItem[] + } + } catch (error) { + return [] + } + return [] +} + +export async function fetchOpentabsContext( + editor: vscode.TextEditor, + cancellationToken: vscode.CancellationToken +): Promise { + const codeChunksCalculated = crossFileContextConfig.numberOfChunkToFetch + + // Step 1: Get relevant cross files to refer + const relevantCrossFilePaths = await getCrossFileCandidates(editor) + + // Step 2: Split files to chunks with upper bound on chunkCount + // We restrict the total number of chunks to improve on latency. + // Chunk linking is required as we want to pass the next chunk value for matched chunk. + let chunkList: Chunk[] = [] + for (const relevantFile of relevantCrossFilePaths) { + const chunks: Chunk[] = await splitFileToChunks(relevantFile, crossFileContextConfig.numberOfLinesEachChunk) + const linkedChunks = linkChunks(chunks) + chunkList.push(...linkedChunks) + if (chunkList.length >= codeChunksCalculated) { + break + } + } + + // it's required since chunkList.push(...) is likely giving us a list of size > 60 + chunkList = chunkList.slice(0, codeChunksCalculated) + + // Step 3: Generate Input chunk (10 lines left of cursor position) + // and Find Best K chunks w.r.t input chunk using BM25 + const inputChunk: Chunk = getInputChunk(editor) + const bestChunks: Chunk[] = findBestKChunkMatches(inputChunk, chunkList, crossFileContextConfig.topK) + + // Step 4: Transform best chunks to supplemental contexts + const supplementalContexts: CodeWhispererSupplementalContextItem[] = [] + let totalLength = 0 + for (const chunk of bestChunks) { + totalLength += chunk.nextContent.length + + if (totalLength > crossFileContextConfig.maximumTotalLength) { + break + } + + supplementalContexts.push({ + filePath: chunk.fileName, + content: chunk.nextContent, + score: chunk.score, + }) + } + + // DO NOT send code chunk with empty content + getLogger().debug(`CodeWhisperer finished fetching crossfile context out of ${relevantCrossFilePaths.length} files`) + return supplementalContexts +} + +function findBestKChunkMatches(chunkInput: Chunk, chunkReferences: Chunk[], k: number): Chunk[] { + const chunkContentList = chunkReferences.map((chunk) => chunk.content) + + // performBM25Scoring returns the output in a sorted order (descending of scores) + const top3: BM25Document[] = new BM25Okapi(chunkContentList).topN(chunkInput.content, crossFileContextConfig.topK) + + return top3.map((doc) => { + // reference to the original metadata since BM25.top3 will sort the result + const chunkIndex = doc.index + const chunkReference = chunkReferences[chunkIndex] + return { + content: chunkReference.content, + fileName: chunkReference.fileName, + nextContent: chunkReference.nextContent, + score: doc.score, + } + }) +} + +/* This extract 10 lines to the left of the cursor from trigger file. + * This will be the inputquery to bm25 matching against list of cross-file chunks + */ +function getInputChunk(editor: vscode.TextEditor) { + const chunkSize = crossFileContextConfig.numberOfLinesEachChunk + const cursorPosition = editor.selection.active + const startLine = Math.max(cursorPosition.line - chunkSize, 0) + const endLine = Math.max(cursorPosition.line - 1, 0) + const inputChunkContent = editor.document.getText( + new vscode.Range(startLine, 0, endLine, editor.document.lineAt(endLine).text.length) + ) + const inputChunk: Chunk = { fileName: editor.document.fileName, content: inputChunkContent, nextContent: '' } + return inputChunk +} + +/** + * Util to decide if we need to fetch crossfile context since CodeWhisperer CrossFile Context feature is gated by userGroup and language level + * @param languageId: VSCode language Identifier + * @returns specifically returning undefined if the langueage is not supported, + * otherwise true/false depending on if the language is fully supported or not belonging to the user group + */ +function getSupplementalContextConfig(languageId: vscode.TextDocument['languageId']): SupplementalContextConfig { + if (!isCrossFileSupported(languageId)) { + return 'none' + } + + const group = FeatureConfigProvider.instance.getProjectContextGroup() + switch (group) { + default: + return 'codemap' + } +} + +/** + * This linking is required from science experimentations to pass the next contnet chunk + * when a given chunk context passes the match in BM25. + * Special handling is needed for last(its next points to its own) and first chunk + */ +export function linkChunks(chunks: Chunk[]) { + const updatedChunks: Chunk[] = [] + + // This additional chunk is needed to create a next pointer to chunk 0. + const firstChunk = chunks[0] + const firstChunkSubContent = firstChunk.content.split('\n').slice(0, 3).join('\n').trimEnd() + const newFirstChunk = { + fileName: firstChunk.fileName, + content: firstChunkSubContent, + nextContent: firstChunk.content, + } + updatedChunks.push(newFirstChunk) + + const n = chunks.length + for (let i = 0; i < n; i++) { + const chunk = chunks[i] + const nextChunk = i < n - 1 ? chunks[i + 1] : chunk + + chunk.nextContent = nextChunk.content + updatedChunks.push(chunk) + } + + return updatedChunks +} + +export async function splitFileToChunks(filePath: string, chunkSize: number): Promise { + const chunks: Chunk[] = [] + + const fileContent = (await fs.readFileText(filePath)).trimEnd() + const lines = fileContent.split('\n') + + for (let i = 0; i < lines.length; i += chunkSize) { + const chunkContent = lines.slice(i, Math.min(i + chunkSize, lines.length)).join('\n') + const chunk = { fileName: filePath, content: chunkContent.trimEnd(), nextContent: '' } + chunks.push(chunk) + } + return chunks +} + +/** + * This function will return relevant cross files sorted by file distance for the given editor file + * by referencing open files, imported files and same package files. + */ +export async function getCrossFileCandidates(editor: vscode.TextEditor): Promise { + const targetFile = editor.document.uri.fsPath + const language = editor.document.languageId as CrossFileSupportedLanguage + const dialects = supportedLanguageToDialects[language] + + /** + * Consider a file which + * 1. is different from the target + * 2. has the same file extension or it's one of the dialect of target file (e.g .js vs. .jsx) + * 3. is not a test file + */ + const unsortedCandidates = await getOpenFilesInWindow(async (candidateFile) => { + return ( + targetFile !== candidateFile && + (path.extname(targetFile) === path.extname(candidateFile) || + (dialects && dialects.has(path.extname(candidateFile)))) && + !(await isTestFile(candidateFile, { languageId: language })) + ) + }) + + return unsortedCandidates + .map((candidate) => { + return { + file: candidate, + fileDistance: getFileDistance(targetFile, candidate), + } + }) + .sort((file1, file2) => { + return file1.fileDistance - file2.fileDistance + }) + .map((fileToDistance) => { + return fileToDistance.file + }) +} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts b/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts new file mode 100644 index 00000000000..a2c77e0b10f --- /dev/null +++ b/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts @@ -0,0 +1,137 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Implementation inspired by https://github.com/dorianbrown/rank_bm25/blob/990470ebbe6b28c18216fd1a8b18fe7446237dd6/rank_bm25.py#L52 + +export interface BM25Document { + content: string + /** The score that the document receives. */ + score: number + + index: number +} + +export abstract class BM25 { + protected readonly corpusSize: number + protected readonly avgdl: number + protected readonly idf: Map = new Map() + protected readonly docLen: number[] = [] + protected readonly docFreqs: Map[] = [] + protected readonly nd: Map = new Map() + + constructor( + protected readonly corpus: string[], + protected readonly tokenizer: (str: string) => string[] = defaultTokenizer, + protected readonly k1: number, + protected readonly b: number, + protected readonly epsilon: number + ) { + this.corpusSize = corpus.length + + let numDoc = 0 + for (const document of corpus.map((document) => { + return tokenizer(document) + })) { + this.docLen.push(document.length) + numDoc += document.length + + const frequencies = new Map() + for (const word of document) { + frequencies.set(word, (frequencies.get(word) || 0) + 1) + } + this.docFreqs.push(frequencies) + + for (const [word, _] of frequencies.entries()) { + this.nd.set(word, (this.nd.get(word) || 0) + 1) + } + } + + this.avgdl = numDoc / this.corpusSize + + this.calIdf(this.nd) + } + + abstract calIdf(nd: Map): void + + abstract score(query: string): BM25Document[] + + topN(query: string, n: number): BM25Document[] { + const notSorted = this.score(query) + const sorted = notSorted.sort((a, b) => b.score - a.score) + return sorted.slice(0, Math.min(n, sorted.length)) + } +} + +export class BM25Okapi extends BM25 { + constructor(corpus: string[], tokenizer: (str: string) => string[] = defaultTokenizer) { + super(corpus, tokenizer, 1.5, 0.75, 0.25) + } + + calIdf(nd: Map): void { + let idfSum = 0 + + const negativeIdfs: string[] = [] + for (const [word, freq] of nd) { + const idf = Math.log(this.corpusSize - freq + 0.5) - Math.log(freq + 0.5) + this.idf.set(word, idf) + idfSum += idf + + if (idf < 0) { + negativeIdfs.push(word) + } + } + + const averageIdf = idfSum / this.idf.size + const eps = this.epsilon * averageIdf + for (const word of negativeIdfs) { + this.idf.set(word, eps) + } + } + + score(query: string): BM25Document[] { + const queryWords = defaultTokenizer(query) + return this.docFreqs.map((docFreq, index) => { + let score = 0 + for (const [_, queryWord] of queryWords.entries()) { + const queryWordFreqForDocument = docFreq.get(queryWord) || 0 + const numerator = (this.idf.get(queryWord) || 0.0) * queryWordFreqForDocument * (this.k1 + 1) + const denominator = + queryWordFreqForDocument + this.k1 * (1 - this.b + (this.b * this.docLen[index]) / this.avgdl) + + score += numerator / denominator + } + + return { + content: this.corpus[index], + score: score, + index: index, + } + }) + } +} + +// TODO: This is a very simple tokenizer, we want to replace this by more sophisticated one. +function defaultTokenizer(content: string): string[] { + const regex = /\w+/g + const words = content.split(' ') + const result = [] + for (const word of words) { + const wordList = findAll(word, regex) + result.push(...wordList) + } + + return result +} + +function findAll(str: string, re: RegExp): string[] { + let match: RegExpExecArray | null + const matches: string[] = [] + + while ((match = re.exec(str)) !== null) { + matches.push(match[0]) + } + + return matches +} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts new file mode 100644 index 00000000000..3a8d66b8b42 --- /dev/null +++ b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts @@ -0,0 +1,139 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fetchSupplementalContextForTest } from './utgUtils' +import { fetchSupplementalContextForSrc } from './crossFileContextUtil' +import { isTestFile } from './codeParsingUtil' +import * as vscode from 'vscode' +import { CancellationError } from '../../../shared/utilities/timeoutUtils' +import { ToolkitError } from '../../../shared/errors' +import { getLogger } from '../../../shared/logger/logger' +import { CodeWhispererSupplementalContext } from '../../models/model' +import * as os from 'os' +import { crossFileContextConfig } from '../../models/constants' +import { BaseLanguageClient } from 'vscode-languageclient' + +export async function fetchSupplementalContext( + editor: vscode.TextEditor, + cancellationToken: vscode.CancellationToken, + languageClient?: BaseLanguageClient +): Promise { + const timesBeforeFetching = Date.now() + + const isUtg = await isTestFile(editor.document.uri.fsPath, { + languageId: editor.document.languageId, + fileContent: editor.document.getText(), + }) + + let supplementalContextPromise: Promise< + Pick | undefined + > + + if (isUtg) { + supplementalContextPromise = fetchSupplementalContextForTest(editor, cancellationToken) + } else { + supplementalContextPromise = fetchSupplementalContextForSrc(editor, cancellationToken, languageClient) + } + + return supplementalContextPromise + .then((value) => { + if (value) { + const resBeforeTruncation = { + isUtg: isUtg, + isProcessTimeout: false, + supplementalContextItems: value.supplementalContextItems.filter( + (item) => item.content.trim().length !== 0 + ), + contentsLength: value.supplementalContextItems.reduce((acc, curr) => acc + curr.content.length, 0), + latency: Date.now() - timesBeforeFetching, + strategy: value.strategy, + } + + return truncateSuppelementalContext(resBeforeTruncation) + } else { + return undefined + } + }) + .catch((err) => { + if (err instanceof ToolkitError && err.cause instanceof CancellationError) { + return { + isUtg: isUtg, + isProcessTimeout: true, + supplementalContextItems: [], + contentsLength: 0, + latency: Date.now() - timesBeforeFetching, + strategy: 'empty', + } + } else { + getLogger().error( + `Fail to fetch supplemental context for target file ${editor.document.fileName}: ${err}` + ) + return undefined + } + }) +} + +/** + * Requirement + * - Maximum 5 supplemental context. + * - Each chunk can't exceed 10240 characters + * - Sum of all chunks can't exceed 20480 characters + */ +export function truncateSuppelementalContext( + context: CodeWhispererSupplementalContext +): CodeWhispererSupplementalContext { + let c = context.supplementalContextItems.map((item) => { + if (item.content.length > crossFileContextConfig.maxLengthEachChunk) { + return { + ...item, + content: truncateLineByLine(item.content, crossFileContextConfig.maxLengthEachChunk), + } + } else { + return item + } + }) + + if (c.length > crossFileContextConfig.maxContextCount) { + c = c.slice(0, crossFileContextConfig.maxContextCount) + } + + let curTotalLength = c.reduce((acc, cur) => { + return acc + cur.content.length + }, 0) + while (curTotalLength >= 20480 && c.length - 1 >= 0) { + const last = c[c.length - 1] + c = c.slice(0, -1) + curTotalLength -= last.content.length + } + + return { + ...context, + supplementalContextItems: c, + contentsLength: curTotalLength, + } +} + +export function truncateLineByLine(input: string, l: number): string { + const maxLength = l > 0 ? l : -1 * l + if (input.length === 0) { + return '' + } + + const shouldAddNewLineBack = input.endsWith(os.EOL) + let lines = input.trim().split(os.EOL) + let curLen = input.length + while (curLen > maxLength && lines.length - 1 >= 0) { + const last = lines[lines.length - 1] + lines = lines.slice(0, -1) + curLen -= last.length + 1 + } + + const r = lines.join(os.EOL) + if (shouldAddNewLineBack) { + return r + os.EOL + } else { + return r + } +} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts b/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts new file mode 100644 index 00000000000..0d33969773e --- /dev/null +++ b/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts @@ -0,0 +1,229 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as path from 'path' +import { fs } from '../../../shared/fs/fs' +import * as vscode from 'vscode' +import { + countSubstringMatches, + extractClasses, + extractFunctions, + isTestFile, + utgLanguageConfig, + utgLanguageConfigs, +} from './codeParsingUtil' +import { ToolkitError } from '../../../shared/errors' +import { supplemetalContextFetchingTimeoutMsg } from '../../models/constants' +import { CancellationError } from '../../../shared/utilities/timeoutUtils' +import { utgConfig } from '../../models/constants' +import { getOpenFilesInWindow } from '../../../shared/utilities/editorUtilities' +import { getLogger } from '../../../shared/logger/logger' +import { CodeWhispererSupplementalContext, CodeWhispererSupplementalContextItem, UtgStrategy } from '../../models/model' + +const utgSupportedLanguages: vscode.TextDocument['languageId'][] = ['java', 'python'] + +type UtgSupportedLanguage = (typeof utgSupportedLanguages)[number] + +function isUtgSupportedLanguage(languageId: vscode.TextDocument['languageId']): languageId is UtgSupportedLanguage { + return utgSupportedLanguages.includes(languageId) +} + +export function shouldFetchUtgContext(languageId: vscode.TextDocument['languageId']): boolean | undefined { + if (!isUtgSupportedLanguage(languageId)) { + return undefined + } + + return languageId === 'java' +} + +/** + * This function attempts to find a focal file for the given trigger file. + * Attempt 1: If naming patterns followed correctly, source file can be found by name referencing. + * Attempt 2: Compare the function and class names of trigger file and all other open files in editor + * to find the closest match. + * Once found the focal file, we split it into multiple pieces as supplementalContext. + * @param editor + * @returns + */ +export async function fetchSupplementalContextForTest( + editor: vscode.TextEditor, + cancellationToken: vscode.CancellationToken +): Promise | undefined> { + const shouldProceed = shouldFetchUtgContext(editor.document.languageId) + + if (!shouldProceed) { + return shouldProceed === undefined ? undefined : { supplementalContextItems: [], strategy: 'empty' } + } + + const languageConfig = utgLanguageConfigs[editor.document.languageId] + + // TODO (Metrics): 1. Total number of calls to fetchSupplementalContextForTest + throwIfCancelled(cancellationToken) + + let crossSourceFile = await findSourceFileByName(editor, languageConfig, cancellationToken) + if (crossSourceFile) { + // TODO (Metrics): 2. Success count for fetchSourceFileByName (find source file by name) + getLogger().debug(`CodeWhisperer finished fetching utg context by file name`) + return { + supplementalContextItems: await generateSupplementalContextFromFocalFile( + crossSourceFile, + 'byName', + cancellationToken + ), + strategy: 'byName', + } + } + throwIfCancelled(cancellationToken) + + crossSourceFile = await findSourceFileByContent(editor, languageConfig, cancellationToken) + if (crossSourceFile) { + // TODO (Metrics): 3. Success count for fetchSourceFileByContent (find source file by content) + getLogger().debug(`CodeWhisperer finished fetching utg context by file content`) + return { + supplementalContextItems: await generateSupplementalContextFromFocalFile( + crossSourceFile, + 'byContent', + cancellationToken + ), + strategy: 'byContent', + } + } + + // TODO (Metrics): 4. Failure count - when unable to find focal file (supplemental context empty) + getLogger().debug(`CodeWhisperer failed to fetch utg context`) + return { + supplementalContextItems: [], + strategy: 'empty', + } +} + +async function generateSupplementalContextFromFocalFile( + filePath: string, + strategy: UtgStrategy, + cancellationToken: vscode.CancellationToken +): Promise { + const fileContent = await fs.readFileText(vscode.Uri.parse(filePath!).fsPath) + + // DO NOT send code chunk with empty content + if (fileContent.trim().length === 0) { + return [] + } + + return [ + { + filePath: filePath, + content: 'UTG\n' + fileContent.slice(0, Math.min(fileContent.length, utgConfig.maxSegmentSize)), + }, + ] +} + +async function findSourceFileByContent( + editor: vscode.TextEditor, + languageConfig: utgLanguageConfig, + cancellationToken: vscode.CancellationToken +): Promise { + const testFileContent = await fs.readFileText(editor.document.fileName) + const testElementList = extractFunctions(testFileContent, languageConfig.functionExtractionPattern) + + throwIfCancelled(cancellationToken) + + testElementList.push(...extractClasses(testFileContent, languageConfig.classExtractionPattern)) + + throwIfCancelled(cancellationToken) + + let sourceFilePath: string | undefined = undefined + let maxMatchCount = 0 + + if (testElementList.length === 0) { + // TODO: Add metrics here, as unable to parse test file using Regex. + return sourceFilePath + } + + const relevantFilePaths = await getRelevantUtgFiles(editor) + + throwIfCancelled(cancellationToken) + + // TODO (Metrics):Add metrics for relevantFilePaths length + for (const filePath of relevantFilePaths) { + throwIfCancelled(cancellationToken) + + const fileContent = await fs.readFileText(filePath) + const elementList = extractFunctions(fileContent, languageConfig.functionExtractionPattern) + elementList.push(...extractClasses(fileContent, languageConfig.classExtractionPattern)) + const matchCount = countSubstringMatches(elementList, testElementList) + if (matchCount > maxMatchCount) { + maxMatchCount = matchCount + sourceFilePath = filePath + } + } + return sourceFilePath +} + +async function getRelevantUtgFiles(editor: vscode.TextEditor): Promise { + const targetFile = editor.document.uri.fsPath + const language = editor.document.languageId + + return await getOpenFilesInWindow(async (candidateFile) => { + return ( + targetFile !== candidateFile && + path.extname(targetFile) === path.extname(candidateFile) && + !(await isTestFile(candidateFile, { languageId: language })) + ) + }) +} + +export function guessSrcFileName( + testFileName: string, + languageId: vscode.TextDocument['languageId'] +): string | undefined { + const languageConfig = utgLanguageConfigs[languageId] + if (!languageConfig) { + return undefined + } + + for (const pattern of languageConfig.testFilenamePattern) { + try { + const match = testFileName.match(pattern) + if (match) { + return match[1] + match[2] + } + } catch (err) { + if (err instanceof Error) { + getLogger().error( + `codewhisperer: error while guessing source file name from file ${testFileName} and pattern ${pattern}: ${err.message}` + ) + } + } + } + + return undefined +} + +async function findSourceFileByName( + editor: vscode.TextEditor, + languageConfig: utgLanguageConfig, + cancellationToken: vscode.CancellationToken +): Promise { + const testFileName = path.basename(editor.document.fileName) + const assumedSrcFileName = guessSrcFileName(testFileName, editor.document.languageId) + if (!assumedSrcFileName) { + return undefined + } + + const sourceFiles = await vscode.workspace.findFiles(`**/${assumedSrcFileName}`) + + throwIfCancelled(cancellationToken) + + if (sourceFiles.length > 0) { + return sourceFiles[0].toString() + } + return undefined +} + +function throwIfCancelled(token: vscode.CancellationToken): void | never { + if (token.isCancellationRequested) { + throw new ToolkitError(supplemetalContextFetchingTimeoutMsg, { cause: new CancellationError('timeout') }) + } +} diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts index 89c04afe572..72f88ab9dc2 100644 --- a/packages/core/src/codewhisperer/util/telemetryHelper.ts +++ b/packages/core/src/codewhisperer/util/telemetryHelper.ts @@ -141,7 +141,7 @@ export class TelemetryHelper { ? this.timeSinceLastModification : undefined, codewhispererTimeSinceLastUserDecision: this.lastTriggerDecisionTime - ? performance.now() - this.lastTriggerDecisionTime + ? Date.now() - this.lastTriggerDecisionTime : undefined, codewhispererTimeToFirstRecommendation: session.timeToFirstRecommendation, codewhispererTriggerType: session.triggerType, @@ -355,7 +355,7 @@ export class TelemetryHelper { ? this.timeSinceLastModification : undefined, codewhispererTimeSinceLastUserDecision: this.lastTriggerDecisionTime - ? performance.now() - this.lastTriggerDecisionTime + ? Date.now() - this.lastTriggerDecisionTime : undefined, codewhispererTimeToFirstRecommendation: session.timeToFirstRecommendation, codewhispererTriggerCharacter: autoTriggerType === 'SpecialCharacters' ? this.triggerChar : undefined, @@ -366,7 +366,7 @@ export class TelemetryHelper { } telemetry.codewhisperer_userTriggerDecision.emit(aggregated) this.prevTriggerDecision = this.getAggregatedSuggestionState(this.sessionDecisions) - this.lastTriggerDecisionTime = performance.now() + this.lastTriggerDecisionTime = Date.now() // When we send a userTriggerDecision for neither Accept nor Reject, service side should not use this value // and client side will set this value to 0.0. @@ -392,6 +392,7 @@ export class TelemetryHelper { generatedLine: generatedLines, numberOfRecommendations: suggestionCount, acceptedCharacterCount: acceptedRecommendationContent.length, + suggestionType: 'COMPLETIONS', } this.resetUserTriggerDecisionTelemetry() @@ -428,7 +429,7 @@ export class TelemetryHelper { } public getLastTriggerDecisionForClassifier() { - if (this.lastTriggerDecisionTime && performance.now() - this.lastTriggerDecisionTime <= 2 * 60 * 1000) { + if (this.lastTriggerDecisionTime && Date.now() - this.lastTriggerDecisionTime <= 2 * 60 * 1000) { return this.prevTriggerDecision } } @@ -556,30 +557,30 @@ export class TelemetryHelper { if (session.preprocessEndTime !== 0) { getLogger().warn(`inline completion preprocessEndTime has been set and not reset correctly`) } - session.preprocessEndTime = performance.now() + session.preprocessEndTime = Date.now() } /** This method is assumed to be invoked first at the start of execution **/ public setInvokeSuggestionStartTime() { this.resetClientComponentLatencyTime() - session.invokeSuggestionStartTime = performance.now() + session.invokeSuggestionStartTime = Date.now() } public setSdkApiCallEndTime() { if (this._sdkApiCallEndTime === 0 && session.sdkApiCallStartTime !== 0) { - this._sdkApiCallEndTime = performance.now() + this._sdkApiCallEndTime = Date.now() } } public setAllPaginationEndTime() { if (this._allPaginationEndTime === 0 && this._sdkApiCallEndTime !== 0) { - this._allPaginationEndTime = performance.now() + this._allPaginationEndTime = Date.now() } } public setFirstSuggestionShowTime() { if (session.firstSuggestionShowTime === 0 && this._sdkApiCallEndTime !== 0) { - session.firstSuggestionShowTime = performance.now() + session.firstSuggestionShowTime = Date.now() } } diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts index b9c1e067b1e..6632819688a 100644 --- a/packages/core/src/codewhispererChat/view/connector/connector.ts +++ b/packages/core/src/codewhispererChat/view/connector/connector.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Timestamp } from 'aws-sdk/clients/apigateway' import { MessagePublisher } from '../../../amazonq/messages/messagePublisher' import { EditorContextCommandType } from '../../commands/registerCommands' import { AuthFollowUpType } from '../../../amazonq/auth/model' @@ -97,7 +96,7 @@ interface StackOverflowMetadata { readonly answerCount: number readonly isAccepted: boolean readonly score: number - readonly lastActivityDate: Timestamp + readonly lastActivityDate: Date } export class SearchView extends UiMessage { diff --git a/packages/core/src/dynamicResources/commands/saveResource.ts b/packages/core/src/dynamicResources/commands/saveResource.ts index 1f696513c65..be395bc7f48 100644 --- a/packages/core/src/dynamicResources/commands/saveResource.ts +++ b/packages/core/src/dynamicResources/commands/saveResource.ts @@ -13,7 +13,7 @@ import { ResourceNode } from '../explorer/nodes/resourceNode' import { ResourceTypeNode } from '../explorer/nodes/resourceTypeNode' import { AwsResourceManager } from '../awsResourceManager' import { CloudControlClient } from '../../shared/clients/cloudControl' -import { CloudControl } from 'aws-sdk' +import { ResourceDescription } from '@aws-sdk/client-cloudcontrol' import globals from '../../shared/extensionGlobals' import { telemetry } from '../../shared/telemetry/telemetry' @@ -224,7 +224,7 @@ export async function updateResource( ) } -function computeDiff(currentDefinition: CloudControl.ResourceDescription, updatedDefinition: string): Operation[] { +function computeDiff(currentDefinition: ResourceDescription, updatedDefinition: string): Operation[] { const current = JSON.parse(currentDefinition.Properties!) const updated = JSON.parse(updatedDefinition) return compare(current, updated) diff --git a/packages/core/src/dynamicResources/explorer/nodes/resourceTypeNode.ts b/packages/core/src/dynamicResources/explorer/nodes/resourceTypeNode.ts index cba2274b162..afc21ff5d16 100644 --- a/packages/core/src/dynamicResources/explorer/nodes/resourceTypeNode.ts +++ b/packages/core/src/dynamicResources/explorer/nodes/resourceTypeNode.ts @@ -15,7 +15,7 @@ import { localize } from '../../../shared/utilities/vsCodeUtils' import { ResourcesNode } from './resourcesNode' import { ResourceNode } from './resourceNode' import { Result } from '../../../shared/telemetry/telemetry' -import { CloudControl } from 'aws-sdk' +import { ResourceDescription } from '@aws-sdk/client-cloudcontrol' import { ResourceTypeMetadata } from '../../model/resources' import { S3Client } from '../../../shared/clients/s3' import { telemetry } from '../../../shared/telemetry/telemetry' @@ -123,15 +123,12 @@ export class ResourceTypeNode extends AWSTreeNodeBase implements LoadMoreNode { }) newResources = response.ResourceDescriptions - ? response.ResourceDescriptions.reduce( - (accumulator: ResourceNode[], current: CloudControl.ResourceDescription) => { - if (current.Identifier) { - accumulator.push(new ResourceNode(this, current.Identifier, this.childContextValue)) - } - return accumulator - }, - [] - ) + ? response.ResourceDescriptions.reduce((accumulator: ResourceNode[], current: ResourceDescription) => { + if (current.Identifier) { + accumulator.push(new ResourceNode(this, current.Identifier, this.childContextValue)) + } + return accumulator + }, []) : [] nextToken = response.NextToken } diff --git a/packages/core/src/eventSchemas/commands/downloadSchemaItemCode.ts b/packages/core/src/eventSchemas/commands/downloadSchemaItemCode.ts index b89a128ba96..0abc1e340e2 100644 --- a/packages/core/src/eventSchemas/commands/downloadSchemaItemCode.ts +++ b/packages/core/src/eventSchemas/commands/downloadSchemaItemCode.ts @@ -5,7 +5,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { Schemas } from 'aws-sdk' +import { PutCodeBindingResponse } from '@aws-sdk/client-schemas' import fs = require('fs') import path = require('path') import * as vscode from 'vscode' @@ -180,10 +180,8 @@ export class SchemaCodeDownloader { export class CodeGenerator { public constructor(public client: SchemaClient) {} - public async generate( - codeDownloadRequest: SchemaCodeDownloadRequestDetails - ): Promise { - let response: Schemas.PutCodeBindingResponse + public async generate(codeDownloadRequest: SchemaCodeDownloadRequestDetails): Promise { + let response: PutCodeBindingResponse try { response = await this.client.putCodeBinding( codeDownloadRequest.language, diff --git a/packages/core/src/eventSchemas/explorer/registryItemNode.ts b/packages/core/src/eventSchemas/explorer/registryItemNode.ts index fcc42e6ea62..8141259b955 100644 --- a/packages/core/src/eventSchemas/explorer/registryItemNode.ts +++ b/packages/core/src/eventSchemas/explorer/registryItemNode.ts @@ -6,7 +6,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { Schemas } from 'aws-sdk' +import { RegistrySummary } from '@aws-sdk/client-schemas' import * as os from 'os' import * as vscode from 'vscode' @@ -25,7 +25,7 @@ export class RegistryItemNode extends AWSTreeNodeBase { public override readonly regionCode: string = this.client.regionCode public constructor( - private registryItemOutput: Schemas.RegistrySummary, + private registryItemOutput: RegistrySummary, private readonly client: SchemaClient ) { super('', vscode.TreeItemCollapsibleState.Collapsed) @@ -56,7 +56,7 @@ export class RegistryItemNode extends AWSTreeNodeBase { }) } - public update(registryItemOutput: Schemas.RegistrySummary): void { + public update(registryItemOutput: RegistrySummary): void { this.registryItemOutput = registryItemOutput this.label = `${this.registryName}` let registryArn = '' diff --git a/packages/core/src/eventSchemas/explorer/schemaItemNode.ts b/packages/core/src/eventSchemas/explorer/schemaItemNode.ts index fcfadf63252..790834e165c 100644 --- a/packages/core/src/eventSchemas/explorer/schemaItemNode.ts +++ b/packages/core/src/eventSchemas/explorer/schemaItemNode.ts @@ -3,8 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Schemas } from 'aws-sdk' - +import { SchemaSummary, SchemaVersionSummary } from '@aws-sdk/client-schemas' import * as os from 'os' import { SchemaClient } from '../../shared/clients/schemaClient' @@ -15,7 +14,7 @@ import { localize } from '../../shared/utilities/vsCodeUtils' export class SchemaItemNode extends AWSTreeNodeBase { public constructor( - private schemaItem: Schemas.SchemaSummary, + private schemaItem: SchemaSummary, public readonly client: SchemaClient, public readonly registryName: string ) { @@ -30,7 +29,7 @@ export class SchemaItemNode extends AWSTreeNodeBase { } } - public update(schemaItem: Schemas.SchemaSummary): void { + public update(schemaItem: SchemaSummary): void { this.schemaItem = schemaItem this.label = this.schemaItem.SchemaName || '' let schemaArn = '' @@ -50,7 +49,7 @@ export class SchemaItemNode extends AWSTreeNodeBase { return response.Content! } - public async listSchemaVersions(): Promise { + public async listSchemaVersions(): Promise { const versions = await toArrayAsync(this.client.listSchemaVersions(this.registryName, this.schemaName)) return versions diff --git a/packages/core/src/eventSchemas/models/schemaCodeLangs.ts b/packages/core/src/eventSchemas/models/schemaCodeLangs.ts index c2bec2e29f6..81814a34cc7 100644 --- a/packages/core/src/eventSchemas/models/schemaCodeLangs.ts +++ b/packages/core/src/eventSchemas/models/schemaCodeLangs.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Runtime } from 'aws-sdk/clients/lambda' +import { Runtime } from '@aws-sdk/client-lambda' import { Set as ImmutableSet } from 'immutable' import { goRuntimes } from '../../lambda/models/samLambdaRuntime' @@ -62,6 +62,7 @@ export function supportsEventBridgeTemplates(runtime: Runtime): boolean { 'python3.11', 'python3.12', 'python3.13', + 'python3.14', 'go1.x', ].includes(runtime) } diff --git a/packages/core/src/eventSchemas/providers/schemasDataProvider.ts b/packages/core/src/eventSchemas/providers/schemasDataProvider.ts index 2df9469d8cb..e0717b6b6d3 100644 --- a/packages/core/src/eventSchemas/providers/schemasDataProvider.ts +++ b/packages/core/src/eventSchemas/providers/schemasDataProvider.ts @@ -4,10 +4,10 @@ */ import * as AWS from '@aws-sdk/types' -import { Schemas } from 'aws-sdk' import { SchemaClient } from '../../shared/clients/schemaClient' import { getLogger, Logger } from '../../shared/logger/logger' import { toArrayAsync } from '../../shared/utilities/collectionUtils' +import { SchemaSummary } from '@aws-sdk/client-schemas' export class Cache { public constructor(public readonly credentialsRegionDataList: credentialsRegionDataListMap[]) {} @@ -26,7 +26,7 @@ export interface regionRegistryMap { export interface registrySchemasMap { registryName: string - schemaList: Schemas.SchemaSummary[] + schemaList: SchemaSummary[] } /** diff --git a/packages/core/src/eventSchemas/utils.ts b/packages/core/src/eventSchemas/utils.ts index a2692d47539..a42d4a26829 100644 --- a/packages/core/src/eventSchemas/utils.ts +++ b/packages/core/src/eventSchemas/utils.ts @@ -6,11 +6,11 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { Schemas } from 'aws-sdk' +import { RegistrySummary, SchemaSummary, SearchSchemaSummary } from '@aws-sdk/client-schemas' import * as vscode from 'vscode' import { SchemaClient } from '../shared/clients/schemaClient' -export async function* listRegistryItems(client: SchemaClient): AsyncIterableIterator { +export async function* listRegistryItems(client: SchemaClient): AsyncIterableIterator { const status = vscode.window.setStatusBarMessage( localize('AWS.message.statusBar.loading.registries', 'Loading Registry Items...') ) @@ -25,7 +25,7 @@ export async function* listRegistryItems(client: SchemaClient): AsyncIterableIte export async function* listSchemaItems( client: SchemaClient, registryName: string -): AsyncIterableIterator { +): AsyncIterableIterator { const status = vscode.window.setStatusBarMessage( localize('AWS.message.statusBar.loading.schemaItems', 'Loading Schema Items...') ) @@ -41,7 +41,7 @@ export async function* searchSchemas( client: SchemaClient, keyword: string, registryName: string -): AsyncIterableIterator { +): AsyncIterableIterator { const status = vscode.window.setStatusBarMessage( localize('AWS.message.statusBar.searching.schemas', 'Searching Schemas...') ) diff --git a/packages/core/src/eventSchemas/vue/searchSchemas.ts b/packages/core/src/eventSchemas/vue/searchSchemas.ts index 37efc145753..7d5c71fa3b0 100644 --- a/packages/core/src/eventSchemas/vue/searchSchemas.ts +++ b/packages/core/src/eventSchemas/vue/searchSchemas.ts @@ -5,7 +5,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { Schemas } from 'aws-sdk' +import { SchemaSummary, SearchSchemaSummary } from '@aws-sdk/client-schemas' import * as vscode from 'vscode' import { downloadSchemaItemCode } from '../commands/downloadSchemaItemCode' import { RegistryItemNode } from '../explorer/registryItemNode' @@ -93,7 +93,7 @@ export class SearchSchemasWebview extends VueWebview { } public async downloadCodeBindings(summary: SchemaVersionedSummary) { - const schemaItem: Schemas.SchemaSummary = { + const schemaItem: SchemaSummary = { SchemaName: getSchemaNameFromTitle(summary.Title), } const schemaItemNode = new SchemaItemNode(schemaItem, this.client, summary.RegistryName) @@ -230,7 +230,7 @@ export async function getSearchResults( return results } -export function getSchemaVersionedSummary(searchSummary: Schemas.SearchSchemaSummary[], prefix: string) { +export function getSchemaVersionedSummary(searchSummary: SearchSchemaSummary[], prefix: string) { const results = searchSummary.map((searchSchemaSummary) => ({ RegistryName: searchSchemaSummary.RegistryName!, Title: prefix.concat(searchSchemaSummary.SchemaName!), diff --git a/packages/core/src/extensionNode.ts b/packages/core/src/extensionNode.ts index 97785456e9b..17cef511ea8 100644 --- a/packages/core/src/extensionNode.ts +++ b/packages/core/src/extensionNode.ts @@ -8,11 +8,13 @@ import * as nls from 'vscode-nls' import * as codecatalyst from './codecatalyst/activation' import { activate as activateAppBuilder } from './awsService/appBuilder/activation' +import { activate as activateCloudFormation } from './awsService/cloudformation/extension' import { activate as activateAwsExplorer } from './awsexplorer/activation' import { activate as activateCloudWatchLogs } from './awsService/cloudWatchLogs/activation' import { activate as activateSchemas } from './eventSchemas/activation' import { activate as activateLambda } from './lambda/activation' import { activate as activateCloudFormationTemplateRegistry } from './shared/cloudformation/activation' + import { AwsContextCommands } from './shared/awsContextCommands' import { getIdeProperties, @@ -42,6 +44,7 @@ import { activate as activateDocumentDb } from './docdb/activation' import { activate as activateIamPolicyChecks } from './awsService/accessanalyzer/activation' import { activate as activateNotifications } from './notifications/activation' import { activate as activateSagemaker } from './awsService/sagemaker/activation' +import { activate as activateSageMakerUnifiedStudio } from './sagemakerunifiedstudio/activation' import { SchemaService } from './shared/schemas' import { AwsResourceManager } from './dynamicResources/awsResourceManager' import globals from './shared/extensionGlobals' @@ -151,6 +154,11 @@ export async function activate(context: vscode.ExtensionContext) { await activateCloudFormationTemplateRegistry(context) + // Start CloudFormation activation in background to avoid blocking other services + activateCloudFormation(context).catch((error) => { + getLogger().error(`CloudFormation activation failed: ${error}`) + }) + await activateAwsExplorer({ context: extContext, regionProvider: globals.regionProvider, @@ -197,6 +205,9 @@ export async function activate(context: vscode.ExtensionContext) { await handleAmazonQInstall() } + + await activateSageMakerUnifiedStudio(extContext) + await activateApplicationComposer(context) await activateThreatComposerEditor(context) diff --git a/packages/core/src/lambda/activation.ts b/packages/core/src/lambda/activation.ts index eaebc17de3b..e0c03986abf 100644 --- a/packages/core/src/lambda/activation.ts +++ b/packages/core/src/lambda/activation.ts @@ -7,7 +7,7 @@ import * as path from 'path' import * as vscode from 'vscode' import * as nls from 'vscode-nls' -import { Lambda } from 'aws-sdk' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' import { deleteLambda } from './commands/deleteLambda' import { uploadLambdaCommand } from './commands/uploadLambda' import { LambdaFunctionNode } from './explorer/lambdaFunctionNode' @@ -18,7 +18,7 @@ import { registerSamDebugInvokeVueCommand, registerSamInvokeVueCommand } from '. import { Commands } from '../shared/vscode/commands2' import { DefaultLambdaClient } from '../shared/clients/lambdaClient' import { copyLambdaUrl } from './commands/copyLambdaUrl' -import { ResourceNode } from '../awsService/appBuilder/explorer/nodes/resourceNode' +import { generateLambdaNodeFromResource, ResourceNode } from '../awsService/appBuilder/explorer/nodes/resourceNode' import { isTreeNode, TreeNode } from '../shared/treeview/resourceTreeDataProvider' import { getSourceNode } from '../shared/utilities/treeNodeUtils' import { tailLogGroup } from '../awsService/cloudWatchLogs/commands/tailLogGroup' @@ -159,7 +159,13 @@ export async function activate(context: ExtContext): Promise { Commands.register('aws.invokeLambda', async (node: LambdaFunctionNode | TreeNode) => { let source: string = 'AwsExplorerRemoteInvoke' if (isTreeNode(node)) { - node = getSourceNode(node) + // if appbuilder, create lambda node on the fly + let tmpNode: LambdaFunctionNode | undefined = getSourceNode(node) + if (!tmpNode) { + // failed to extract, meaning this is appbuilder function node + tmpNode = await generateLambdaNodeFromResource(node.resource as any) + } + node = tmpNode source = 'AppBuilderRemoteInvoke' } await invokeRemoteLambda(context, { @@ -227,14 +233,17 @@ export async function activate(context: ExtContext): Promise { Commands.register('aws.launchDebugConfigForm', async (node: ResourceNode) => registerSamDebugInvokeVueCommand(context.extensionContext, { resource: node }) ), - Commands.register('aws.appBuilder.tailLogs', async (node: LambdaFunctionNode | TreeNode) => { - let functionConfiguration: Lambda.FunctionConfiguration + let functionConfiguration: FunctionConfiguration try { - const sourceNode = getSourceNode(node) - functionConfiguration = sourceNode.configuration + let tmpNode: LambdaFunctionNode | undefined = getSourceNode(node) + if (!tmpNode && isTreeNode(node)) { + // failed to extract, meaning this is appbuilder function node + tmpNode = await generateLambdaNodeFromResource(node.resource as any) + } + functionConfiguration = tmpNode.configuration const logGroupInfo = { - regionName: sourceNode.regionCode, + regionName: tmpNode.regionCode, groupName: getFunctionLogGroupName(functionConfiguration), } diff --git a/packages/core/src/lambda/commands/copyLambdaUrl.ts b/packages/core/src/lambda/commands/copyLambdaUrl.ts index d15a96a7d62..835525d0610 100644 --- a/packages/core/src/lambda/commands/copyLambdaUrl.ts +++ b/packages/core/src/lambda/commands/copyLambdaUrl.ts @@ -10,7 +10,7 @@ import { copyToClipboard } from '../../shared/utilities/messages' import { addCodiconToString } from '../../shared/utilities/textUtilities' import { createQuickPick, QuickPickPrompter } from '../../shared/ui/pickerPrompter' import { isValidResponse } from '../../shared/wizards/wizard' -import { FunctionUrlConfigList } from 'aws-sdk/clients/lambda' +import { FunctionUrlConfig } from '@aws-sdk/client-lambda' import { CancellationError } from '../../shared/utilities/timeoutUtils' import { lambdaFunctionUrlConfigUrl } from '../../shared/constants' @@ -40,7 +40,7 @@ export async function copyLambdaUrl( } } -async function _quickPickUrl(configList: FunctionUrlConfigList): Promise { +async function _quickPickUrl(configList: FunctionUrlConfig[]): Promise { const res = await createLambdaFuncUrlPrompter(configList).prompt() if (!isValidResponse(res)) { throw new CancellationError('user') @@ -48,10 +48,12 @@ async function _quickPickUrl(configList: FunctionUrlConfigList): Promise { - const items = configList.map((c) => ({ - label: c.FunctionArn, - data: c.FunctionUrl, - })) +export function createLambdaFuncUrlPrompter(configList: FunctionUrlConfig[]): QuickPickPrompter { + const items = configList + .filter((c) => c.FunctionArn && c.FunctionUrl) + .map((c) => ({ + label: c.FunctionArn!, + data: c.FunctionUrl!, + })) return createQuickPick(items, { title: 'Select function to copy url from.' }) } diff --git a/packages/core/src/lambda/commands/createNewSamApp.ts b/packages/core/src/lambda/commands/createNewSamApp.ts index c53edf2518b..7801976c435 100644 --- a/packages/core/src/lambda/commands/createNewSamApp.ts +++ b/packages/core/src/lambda/commands/createNewSamApp.ts @@ -43,7 +43,8 @@ import { getIdeProperties, getDebugNewSamAppDocUrl, getLaunchConfigDocUrl } from import { checklogs } from '../../shared/localizedText' import globals from '../../shared/extensionGlobals' import { telemetry } from '../../shared/telemetry/telemetry' -import { LambdaArchitecture, Result, Runtime } from '../../shared/telemetry/telemetry' +import { LambdaArchitecture, Result, Runtime as TelemetryRuntime } from '../../shared/telemetry/telemetry' +import { Runtime } from '@aws-sdk/client-lambda' import { getTelemetryReason, getTelemetryResult } from '../../shared/errors' import { openUrl, replaceVscodeVars } from '../../shared/utilities/vsCodeUtils' import { fs } from '../../shared/fs/fs' @@ -88,7 +89,7 @@ export async function resumeCreateNewSamApp( extContext, folder, templateUri, - samInitState?.isImage ? (samInitState?.runtime as Runtime | undefined) : undefined + samInitState?.isImage ? samInitState?.runtime : undefined ) const tryOpenReadme = await writeToolkitReadme(readmeUri.fsPath, configs) if (tryOpenReadme) { @@ -112,7 +113,7 @@ export async function resumeCreateNewSamApp( lambdaArchitecture: arch, result: createResult, reason: reason, - runtime: samInitState?.runtime as Runtime, + runtime: samInitState?.runtime as TelemetryRuntime, version: samVersion, }) } @@ -194,7 +195,7 @@ export async function createNewSamApplication( initArguments.baseImage = `amazon/${createRuntime}-base` } else { lambdaPackageType = 'Zip' - initArguments.runtime = createRuntime + initArguments.runtime! = createRuntime // in theory, templates could be provided with image-based lambdas, but that is currently not supported by SAM initArguments.template = config.template } @@ -348,7 +349,7 @@ export async function createNewSamApplication( lambdaArchitecture: initArguments?.architecture, result: createResult, reason: reason, - runtime: createRuntime, + runtime: createRuntime as TelemetryRuntime, version: samVersion, }) } diff --git a/packages/core/src/lambda/commands/deleteLambda.ts b/packages/core/src/lambda/commands/deleteLambda.ts index 29dea130f66..c6290eab3c6 100644 --- a/packages/core/src/lambda/commands/deleteLambda.ts +++ b/packages/core/src/lambda/commands/deleteLambda.ts @@ -10,7 +10,7 @@ import * as localizedText from '../../shared/localizedText' import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' import { Result } from '../../shared/telemetry/telemetry' import { showConfirmationMessage, showViewLogsMessage } from '../../shared/utilities/messages' -import { FunctionConfiguration } from 'aws-sdk/clients/lambda' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' import { getLogger } from '../../shared/logger/logger' import { telemetry } from '../../shared/telemetry/telemetry' diff --git a/packages/core/src/lambda/commands/uploadLambda.ts b/packages/core/src/lambda/commands/uploadLambda.ts index 6bfd3777463..98149674587 100644 --- a/packages/core/src/lambda/commands/uploadLambda.ts +++ b/packages/core/src/lambda/commands/uploadLambda.ts @@ -27,7 +27,7 @@ import { StepEstimator, Wizard, WIZARD_BACK } from '../../shared/wizards/wizard' import { createSingleFileDialog } from '../../shared/ui/common/openDialog' import { Prompter, PromptResult } from '../../shared/ui/prompter' import { ToolkitError } from '../../shared/errors' -import { FunctionConfiguration } from 'aws-sdk/clients/lambda' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' import globals from '../../shared/extensionGlobals' import { toArrayAsync } from '../../shared/utilities/collectionUtils' import { fromExtensionManifest } from '../../shared/settings' diff --git a/packages/core/src/lambda/explorer/cloudFormationNodes.ts b/packages/core/src/lambda/explorer/cloudFormationNodes.ts index e2d9e9aae27..daf49a06a47 100644 --- a/packages/core/src/lambda/explorer/cloudFormationNodes.ts +++ b/packages/core/src/lambda/explorer/cloudFormationNodes.ts @@ -6,7 +6,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { CloudFormation, Lambda } from 'aws-sdk' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' import * as os from 'os' import * as vscode from 'vscode' import { CloudFormationClient, StackSummary } from '../../shared/clients/cloudFormation' @@ -15,6 +15,7 @@ import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode' import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode' +import { AWSCommandTreeNode } from '../../shared/treeview/nodes/awsCommandTreeNode' import { makeChildrenNodes } from '../../shared/treeview/utils' import { intersection, toArrayAsync, toMap, toMapAsync, updateInPlace } from '../../shared/utilities/collectionUtils' import { listCloudFormationStacks, listLambdaFunctions } from '../utils' @@ -40,11 +41,38 @@ export class CloudFormationNode extends AWSTreeNodeBase { getChildNodes: async () => { await this.updateChildren() - return [...this.stackNodes.values()] + const panelNode = new AWSCommandTreeNode( + this, + '✨ Try the new CloudFormation panel', + 'aws.cloudformation.focus', + undefined, + 'Open the enhanced CloudFormation panel with improved features' + ) + panelNode.iconPath = getIcon('vscode-star-full') + + return [panelNode, ...this.stackNodes.values()] + }, + getNoChildrenPlaceholderNode: async () => { + const panelNode = new AWSCommandTreeNode( + this, + '✨ Try the new CloudFormation panel', + 'aws.cloudformation.focus', + undefined, + 'Open the enhanced CloudFormation panel with improved features' + ) + panelNode.iconPath = getIcon('vscode-star-full') + return panelNode + }, + sort: (nodeA, nodeB) => { + // Keep the panel node at the top + if (nodeA instanceof AWSCommandTreeNode) { + return -1 + } + if (nodeB instanceof AWSCommandTreeNode) { + return 1 + } + return nodeA.stackName.localeCompare(nodeB.stackName) }, - getNoChildrenPlaceholderNode: async () => - new PlaceholderNode(this, localize('AWS.explorerNode.cloudformation.noStacks', '[No Stacks found]')), - sort: (nodeA, nodeB) => nodeA.stackName.localeCompare(nodeB.stackName), }) } @@ -78,7 +106,7 @@ export class CloudFormationStackNode extends AWSTreeNodeBase implements AWSResou this.iconPath = getIcon('aws-cloudformation-stack') } - public get stackId(): CloudFormation.StackId | undefined { + public get stackId(): string | undefined { return this.stackSummary.StackId } @@ -94,7 +122,7 @@ export class CloudFormationStackNode extends AWSTreeNodeBase implements AWSResou return this.stackName } - public get stackName(): CloudFormation.StackName { + public get stackName(): string { return this.stackSummary.StackName } @@ -122,7 +150,7 @@ export class CloudFormationStackNode extends AWSTreeNodeBase implements AWSResou private async updateChildren(): Promise { const resources: string[] = await this.resolveLambdaResources() - const functions: Map = toMap( + const functions: Map = toMap( await toArrayAsync(listLambdaFunctions(this.lambdaClient)), (functionInfo) => functionInfo.FunctionName ) @@ -151,7 +179,7 @@ export class CloudFormationStackNode extends AWSTreeNodeBase implements AWSResou function makeCloudFormationLambdaFunctionNode( parent: AWSTreeNodeBase, regionCode: string, - configuration: Lambda.FunctionConfiguration + configuration: FunctionConfiguration ): LambdaFunctionNode { const node = new LambdaFunctionNode(parent, regionCode, configuration, contextValueCloudformationLambdaFunction) diff --git a/packages/core/src/lambda/explorer/lambdaCapacityProviderNode.ts b/packages/core/src/lambda/explorer/lambdaCapacityProviderNode.ts new file mode 100644 index 00000000000..17a5562eb10 --- /dev/null +++ b/packages/core/src/lambda/explorer/lambdaCapacityProviderNode.ts @@ -0,0 +1,46 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getIcon } from '../../shared/icons' + +import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode' +import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' +import globals from '../../shared/extensionGlobals' +import { ToolkitError } from '../../shared/errors' + +export const contextValueLambdaCapacityProvider = 'awsCapacityProviderNode' + +export class LambdaCapacityProviderNode extends AWSTreeNodeBase implements AWSResourceNode { + public constructor( + public override readonly regionCode: string, + public readonly deployedResource: any, + public override readonly contextValue?: string + ) { + super( + deployedResource.LogicalResourceId, + contextValue === contextValueLambdaCapacityProvider + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None + ) + this.iconPath = getIcon('vscode-gear') + this.contextValue = contextValueLambdaCapacityProvider + } + + public get name() { + return this.deployedResource.LogicalResourceId + } + private get accountId(): string { + const accountId = globals.awsContext.getCredentialAccountId() + if (!accountId) { + throw new ToolkitError('Aws account ID not found') + } + return accountId + } + + public get arn() { + return `arn:aws:lambda:${this.regionCode}:${this.accountId}:capacity-provider:${this.deployedResource.PhysicalResourceId}` + } +} diff --git a/packages/core/src/lambda/explorer/lambdaFunctionNode.ts b/packages/core/src/lambda/explorer/lambdaFunctionNode.ts index 03cb9210aaa..3d2f05a99fa 100644 --- a/packages/core/src/lambda/explorer/lambdaFunctionNode.ts +++ b/packages/core/src/lambda/explorer/lambdaFunctionNode.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Lambda } from 'aws-sdk' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' import * as os from 'os' import * as vscode from 'vscode' import { getIcon } from '../../shared/icons' @@ -21,18 +21,28 @@ import { LambdaFunctionFileNode } from './lambdaFunctionFileNode' export const contextValueLambdaFunction = 'awsRegionFunctionNode' export const contextValueLambdaFunctionImportable = 'awsRegionFunctionNodeDownloadable' +// Without "Convert to SAM application" +export const contextValueLambdaFunctionDownloadOnly = 'awsRegionFunctionNodeDownloadableOnly' + +function isLambdaFunctionDownloadable(contextValue?: string): boolean { + return ( + contextValue === contextValueLambdaFunctionImportable || contextValue === contextValueLambdaFunctionDownloadOnly + ) +} export class LambdaFunctionNode extends AWSTreeNodeBase implements AWSResourceNode { public constructor( public readonly parent: AWSTreeNodeBase, public override readonly regionCode: string, - public configuration: Lambda.FunctionConfiguration, + public configuration: FunctionConfiguration, public override readonly contextValue?: string, - public localDir?: string + public localDir?: string, + public projectRoot?: vscode.Uri, + public logicalId?: string ) { super( `${configuration.FunctionArn}`, - contextValue === contextValueLambdaFunctionImportable + isLambdaFunctionDownloadable(contextValue) ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None ) @@ -42,7 +52,7 @@ export class LambdaFunctionNode extends AWSTreeNodeBase implements AWSResourceNo this.contextValue = contextValue } - public update(configuration: Lambda.FunctionConfiguration): void { + public update(configuration: FunctionConfiguration): void { this.configuration = configuration this.label = this.configuration.FunctionName || '' this.tooltip = `${this.configuration.FunctionName}${os.EOL}${this.configuration.FunctionArn}` @@ -72,7 +82,7 @@ export class LambdaFunctionNode extends AWSTreeNodeBase implements AWSResourceNo } public override async getChildren(): Promise { - if (!(this.contextValue === contextValueLambdaFunctionImportable)) { + if (!isLambdaFunctionDownloadable(this.contextValue)) { return [] } diff --git a/packages/core/src/lambda/explorer/lambdaNodes.ts b/packages/core/src/lambda/explorer/lambdaNodes.ts index 077572feda7..dd753ce5594 100644 --- a/packages/core/src/lambda/explorer/lambdaNodes.ts +++ b/packages/core/src/lambda/explorer/lambdaNodes.ts @@ -6,7 +6,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { Lambda } from 'aws-sdk' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' import * as vscode from 'vscode' import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' @@ -14,13 +14,15 @@ import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode' import { makeChildrenNodes } from '../../shared/treeview/utils' import { toArrayAsync, toMap, updateInPlace } from '../../shared/utilities/collectionUtils' -import { listLambdaFunctions } from '../utils' +import { listLambdaFunctions, isHotReloadingFunction } from '../utils' import { contextValueLambdaFunction, contextValueLambdaFunctionImportable, + contextValueLambdaFunctionDownloadOnly, LambdaFunctionNode, } from './lambdaFunctionNode' import { samLambdaImportableRuntimes } from '../models/samLambdaRuntime' +import { isLocalStackConnection } from '../../auth/utils' /** * An AWS Explorer node representing the Lambda Service. @@ -52,7 +54,7 @@ export class LambdaNode extends AWSTreeNodeBase { } public async updateChildren(): Promise { - const functions: Map = toMap( + const functions: Map = toMap( await toArrayAsync(listLambdaFunctions(this.client)), (configuration) => configuration.FunctionName ) @@ -69,11 +71,17 @@ export class LambdaNode extends AWSTreeNodeBase { function makeLambdaFunctionNode( parent: AWSTreeNodeBase, regionCode: string, - configuration: Lambda.FunctionConfiguration + configuration: FunctionConfiguration ): LambdaFunctionNode { - const contextValue = samLambdaImportableRuntimes.contains(configuration.Runtime ?? '') - ? contextValueLambdaFunctionImportable - : contextValueLambdaFunction + let contextValue = contextValueLambdaFunction + const isImportableRuntime = configuration.Runtime && samLambdaImportableRuntimes.contains(configuration.Runtime) + if (isLocalStackConnection()) { + if (isImportableRuntime && !isHotReloadingFunction(configuration?.CodeSha256)) { + contextValue = contextValueLambdaFunctionDownloadOnly + } + } else if (isImportableRuntime) { + contextValue = contextValueLambdaFunctionImportable + } const node = new LambdaFunctionNode(parent, regionCode, configuration, contextValue) return node diff --git a/packages/core/src/lambda/models/samLambdaRuntime.ts b/packages/core/src/lambda/models/samLambdaRuntime.ts index 06e35dbcd2b..4c7ea9be596 100644 --- a/packages/core/src/lambda/models/samLambdaRuntime.ts +++ b/packages/core/src/lambda/models/samLambdaRuntime.ts @@ -7,7 +7,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() import * as vscode from 'vscode' -import { Runtime } from 'aws-sdk/clients/lambda' +import { Runtime } from '@aws-sdk/client-lambda' import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable' import { isCloud9 } from '../../shared/extensionUtilities' import { PrompterButtons } from '../../shared/ui/buttons' @@ -30,7 +30,8 @@ export type RuntimePackageType = 'Image' | 'Zip' // TODO: Consolidate all of the runtime constructs into a single > map // We should be able to eliminate a fair amount of redundancy with that. export const nodeJsRuntimes: ImmutableSet = ImmutableSet([ - 'nodejs22.x', + 'nodejs24.x' as Runtime, + 'nodejs22.x' as Runtime, 'nodejs20.x', 'nodejs18.x', 'nodejs16.x', @@ -51,7 +52,8 @@ export function getNodeMajorVersion(version?: string): number | undefined { } export const pythonRuntimes: ImmutableSet = ImmutableSet([ - 'python3.13', + 'python3.14' as Runtime, + 'python3.13' as Runtime, 'python3.12', 'python3.11', 'python3.10', @@ -66,9 +68,13 @@ export const javaRuntimes: ImmutableSet = ImmutableSet([ 'java8', 'java8.al2', 'java21', + 'java25' as Runtime, ]) export const dotNetRuntimes: ImmutableSet = ImmutableSet(['dotnet6', 'dotnet8']) -export const rubyRuntimes: ImmutableSet = ImmutableSet(['ruby3.2', 'ruby3.3', 'ruby3.4']) +export const rubyRuntimes: ImmutableSet = ImmutableSet(['ruby3.2', 'ruby3.3', 'ruby3.4' as Runtime]) + +// Image runtimes are not a direct subset of valid ZIP lambda types +const dotnet50 = 'dotnet5.0' as Runtime /** * Deprecated runtimes can be found at https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html @@ -91,12 +97,12 @@ export const deprecatedRuntimes: ImmutableSet = ImmutableSet([ 'ruby2.7', ]) const defaultRuntimes = ImmutableMap([ - [RuntimeFamily.NodeJS, 'nodejs22.x'], - [RuntimeFamily.Python, 'python3.13'], + [RuntimeFamily.NodeJS, 'nodejs24.x' as Runtime], + [RuntimeFamily.Python, 'python3.14' as Runtime], [RuntimeFamily.DotNet, 'dotnet8'], [RuntimeFamily.Go, 'go1.x'], - [RuntimeFamily.Java, 'java21'], - [RuntimeFamily.Ruby, 'ruby3.3'], + [RuntimeFamily.Java, 'java25' as Runtime], + [RuntimeFamily.Ruby, 'ruby3.4' as Runtime], ]) export const mapFamilyToDebugType = ImmutableMap([ @@ -120,7 +126,7 @@ export const samZipLambdaRuntimes: ImmutableSet = ImmutableSet.union([ export const samArmLambdaRuntimes: ImmutableSet = ImmutableSet([ 'python3.9', 'python3.8', - 'nodejs22.x', + 'nodejs22.x' as Runtime, 'nodejs20.x', 'nodejs18.x', 'nodejs16.x', @@ -145,8 +151,6 @@ export function samLambdaCreatableRuntimes(cloud9: boolean = isCloud9()): Immuta return cloud9 ? cloud9SupportedRuntimes : samZipLambdaRuntimes } -// Image runtimes are not a direct subset of valid ZIP lambda types -const dotnet50 = 'dotnet5.0' export function samImageLambdaRuntimes(cloud9: boolean = isCloud9()): ImmutableSet { // Note: SAM also supports ruby, but Toolkit does not. return ImmutableSet([...samLambdaCreatableRuntimes(cloud9), ...(cloud9 ? [] : [dotnet50])]) @@ -172,7 +176,7 @@ export function getDependencyManager(runtime: Runtime): DependencyManager[] { throw new Error(`Runtime ${runtime} does not have an associated DependencyManager`) } -export function getFamily(runtime: string): RuntimeFamily { +export function getFamily(runtime: Runtime): RuntimeFamily { if (deprecatedRuntimes.has(runtime)) { handleDeprecatedRuntime(runtime) } else if (nodeJsRuntimes.has(runtime)) { @@ -248,7 +252,7 @@ export function getRuntimeFamily(langId: string): RuntimeFamily { /** * Provides the default runtime for a given `RuntimeFamily` or undefined if the runtime is invalid. */ -export function getDefaultRuntime(runtime: RuntimeFamily): string | undefined { +export function getDefaultRuntime(runtime: RuntimeFamily): Runtime | undefined { return defaultRuntimes.get(runtime) } diff --git a/packages/core/src/lambda/models/samTemplates.ts b/packages/core/src/lambda/models/samTemplates.ts index 5ec112a7dc4..ace9c93faf6 100644 --- a/packages/core/src/lambda/models/samTemplates.ts +++ b/packages/core/src/lambda/models/samTemplates.ts @@ -6,7 +6,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() import * as semver from 'semver' -import { Runtime } from 'aws-sdk/clients/lambda' +import { Runtime } from '@aws-sdk/client-lambda' import { Set as ImmutableSet } from 'immutable' import { supportsEventBridgeTemplates } from '../../../src/eventSchemas/models/schemaCodeLangs' import { RuntimePackageType } from './samLambdaRuntime' diff --git a/packages/core/src/lambda/remoteDebugging/lambdaDebugger.ts b/packages/core/src/lambda/remoteDebugging/lambdaDebugger.ts new file mode 100644 index 00000000000..152d5913737 --- /dev/null +++ b/packages/core/src/lambda/remoteDebugging/lambdaDebugger.ts @@ -0,0 +1,75 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import globals from '../../shared/extensionGlobals' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' +import { getLogger } from '../../shared/logger/logger' + +const logger = getLogger() + +export const remoteDebugSnapshotString = 'aws.lambda.remoteDebugSnapshot' + +export interface DebugConfig { + functionArn: string + functionName: string + port: number | undefined + localRoot: string + remoteRoot: string + skipFiles: string[] + shouldPublishVersion: boolean + lambdaRuntime?: string // Lambda runtime (e.g., nodejs18.x) + debuggerRuntime?: string // VS Code debugger runtime (e.g., node) + outFiles?: string[] + sourceMap?: boolean + justMyCode?: boolean + projectName?: string + otherDebugParams?: string + lambdaTimeout?: number + layerArn?: string + handlerFile?: string + samFunctionLogicalId?: string // SAM function logical ID for auto-detecting outFiles + samProjectRoot?: vscode.Uri // SAM project root for auto-detecting outFiles + isLambdaRemote: boolean // false if LocalStack connection +} + +/** + * Interface for debugging AWS Lambda functions remotely. + * + * This interface defines the contract for implementing remote debugging + * for Lambda functions. + * + * Implementations of this interface handle the lifecycle of remote debugging sessions, + * including checking health, set up, necessary deployment, and later clean up + */ +export interface LambdaDebugger { + checkHealth(): Promise + setup( + progress: vscode.Progress<{ message?: string; increment?: number }>, + functionConfig: FunctionConfiguration, + region: string + ): Promise + waitForSetup( + progress: vscode.Progress<{ message?: string; increment?: number }>, + functionConfig: FunctionConfiguration, + region: string + ): Promise + waitForFunctionUpdates(progress: vscode.Progress<{ message?: string; increment?: number }>): Promise + cleanup(functionConfig: FunctionConfiguration): Promise +} + +// this should be called when the debug session is started +export async function persistLambdaSnapshot(config: FunctionConfiguration | undefined): Promise { + try { + await globals.globalState.update(remoteDebugSnapshotString, config) + } catch (error) { + // TODO raise toolkit error + logger.error(`Error persisting debug sessions: ${error}`) + } +} + +export function getLambdaSnapshot(): FunctionConfiguration | undefined { + return globals.globalState.get(remoteDebugSnapshotString) +} diff --git a/packages/core/src/lambda/remoteDebugging/ldkClient.ts b/packages/core/src/lambda/remoteDebugging/ldkClient.ts index 915e150b039..43360506ed9 100644 --- a/packages/core/src/lambda/remoteDebugging/ldkClient.ts +++ b/packages/core/src/lambda/remoteDebugging/ldkClient.ts @@ -4,13 +4,20 @@ */ import * as vscode from 'vscode' -import { IoTSecureTunneling, Lambda } from 'aws-sdk' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' +import { + CloseTunnelCommand, + IoTSecureTunnelingClient, + ListTunnelsCommand, + OpenTunnelCommand, + RotateTunnelAccessTokenCommand, +} from '@aws-sdk/client-iotsecuretunneling' import { getClientId } from '../../shared/telemetry/util' import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' import { LocalProxy } from './localProxy' import globals from '../../shared/extensionGlobals' import { getLogger } from '../../shared/logger/logger' -import { getIoTSTClientWithAgent, getLambdaClientWithAgent } from './utils' +import { getIoTSTClientWithAgent, getLambdaClientWithAgent, getLambdaDebugUserAgentPairs } from './utils' import { ToolkitError } from '../../shared/errors' import * as nls from 'vscode-nls' @@ -26,7 +33,7 @@ export function isTunnelInfo(data: TunnelInfo): data is TunnelInfo { ) } -interface TunnelInfo { +export interface TunnelInfo { tunnelID: string sourceToken: string destinationToken: string @@ -34,9 +41,9 @@ interface TunnelInfo { async function callUpdateFunctionConfiguration( lambda: DefaultLambdaClient, - config: Lambda.FunctionConfiguration, + config: FunctionConfiguration, waitForUpdate: boolean -): Promise { +): Promise { // Update function configuration back to original values return await lambda.updateFunctionConfiguration( { @@ -61,7 +68,7 @@ export class LdkClient { private localProxy: LocalProxy | undefined private static instanceCreating = false private lambdaClientCache: Map = new Map() - private iotSTClientCache: Map = new Map() + private iotSTClientCache: Map = new Map() constructor() {} @@ -92,14 +99,14 @@ export class LdkClient { */ private getLambdaClient(region: string): DefaultLambdaClient { if (!this.lambdaClientCache.has(region)) { - this.lambdaClientCache.set(region, getLambdaClientWithAgent(region)) + this.lambdaClientCache.set(region, getLambdaClientWithAgent(region, getLambdaDebugUserAgentPairs())) } return this.lambdaClientCache.get(region)! } - private async getIoTSTClient(region: string): Promise { + private getIoTSTClient(region: string): IoTSecureTunnelingClient { if (!this.iotSTClientCache.has(region)) { - this.iotSTClientCache.set(region, await getIoTSTClientWithAgent(region)) + this.iotSTClientCache.set(region, getIoTSTClientWithAgent(region)) } return this.iotSTClientCache.get(region)! } @@ -124,13 +131,13 @@ export class LdkClient { const vscodeUuid = getClientId(globals.globalState) // Create IoTSecureTunneling client - const iotSecureTunneling = await this.getIoTSTClient(region) + const iotSecureTunneling = this.getIoTSTClient(region) // Define tunnel identifier const tunnelIdentifier = `RemoteDebugging+${vscodeUuid}` const timeoutInMinutes = 720 // List existing tunnels - const listTunnelsResponse = await iotSecureTunneling.listTunnels({}).promise() + const listTunnelsResponse = await iotSecureTunneling.send(new ListTunnelsCommand({})) // Find tunnel with our identifier const existingTunnel = listTunnelsResponse.tunnelSummaries?.find( @@ -150,20 +157,20 @@ export class LdkClient { return rotateResponse } else { // Close tunnel if less than 15 minutes remaining - await iotSecureTunneling - .closeTunnel({ + await iotSecureTunneling.send( + new CloseTunnelCommand({ tunnelId: existingTunnel.tunnelId, delete: false, }) - .promise() + ) getLogger().info(`Closed tunnel ${existingTunnel.tunnelId} with less than 15 minutes remaining`) } } // Create new tunnel - const openTunnelResponse = await iotSecureTunneling - .openTunnel({ + const openTunnelResponse = await iotSecureTunneling.send( + new OpenTunnelCommand({ description: tunnelIdentifier, timeoutConfig: { maxLifetimeTimeoutMinutes: timeoutInMinutes, // 12 hours @@ -172,7 +179,7 @@ export class LdkClient { services: ['WSS'], }, }) - .promise() + ) getLogger().info(`Created new tunnel with ID: ${openTunnelResponse.tunnelId}`) @@ -189,13 +196,13 @@ export class LdkClient { // Refresh tunnel tokens async refreshTunnelTokens(tunnelId: string, region: string): Promise { try { - const iotSecureTunneling = await this.getIoTSTClient(region) - const rotateResponse = await iotSecureTunneling - .rotateTunnelAccessToken({ + const iotSecureTunneling = this.getIoTSTClient(region) + const rotateResponse = await iotSecureTunneling.send( + new RotateTunnelAccessTokenCommand({ tunnelId: tunnelId, clientMode: 'ALL', }) - .promise() + ) return { tunnelID: tunnelId, @@ -207,7 +214,7 @@ export class LdkClient { } } - async getFunctionDetail(functionArn: string): Promise { + async getFunctionDetail(functionArn: string): Promise { try { const region = getRegionFromArn(functionArn) if (!region) { @@ -220,7 +227,7 @@ export class LdkClient { return undefined } const client = this.getLambdaClient(region) - const configuration = (await client.getFunction(functionArn)).Configuration as Lambda.FunctionConfiguration + const configuration = (await client.getFunction(functionArn)).Configuration as FunctionConfiguration // get function detail // return function detail return configuration @@ -237,7 +244,7 @@ export class LdkClient { // 3: adding two param to lambda environment variable // {AWS_LAMBDA_EXEC_WRAPPER:/opt/bin/ldk_wrapper, AWS_LDK_DESTINATION_TOKEN: destinationToken } async createDebugDeployment( - config: Lambda.FunctionConfiguration, + config: FunctionConfiguration, destinationToken: string, lambdaTimeout: number, shouldPublishVersion: boolean, @@ -302,6 +309,10 @@ export class LdkClient { updatedEnv.ORIGINAL_AWS_LAMBDA_EXEC_WRAPPER = currentEnv['AWS_LAMBDA_EXEC_WRAPPER'] } + if (getLogger().logLevelEnabled('debug')) { + updatedEnv.RUST_LOG = 'debug' + } + // Create Lambda client using AWS SDK const lambda = this.getLambdaClient(region) @@ -311,7 +322,7 @@ export class LdkClient { } // Create a temporary config for the update - const updateConfig: Lambda.FunctionConfiguration = { + const updateConfig: FunctionConfiguration = { FunctionName: config.FunctionName, Timeout: lambdaTimeout ?? 900, // 15 minutes Layers: updatedLayers.map((arn) => ({ Arn: arn })), @@ -355,7 +366,7 @@ export class LdkClient { // we are 1: reverting timeout to it's original snapshot // 2: reverting layer status according to it's original snapshot // 3: reverting environment back to it's original snapshot - async removeDebugDeployment(config: Lambda.FunctionConfiguration, check: boolean = true): Promise { + async removeDebugDeployment(config: FunctionConfiguration, check: boolean = true): Promise { try { if (!config.FunctionArn || !config.FunctionName) { throw new Error('Function ARN is missing') diff --git a/packages/core/src/lambda/remoteDebugging/ldkController.ts b/packages/core/src/lambda/remoteDebugging/ldkController.ts index 55a777fdc3d..dc04c9bd164 100644 --- a/packages/core/src/lambda/remoteDebugging/ldkController.ts +++ b/packages/core/src/lambda/remoteDebugging/ldkController.ts @@ -6,23 +6,25 @@ import * as vscode from 'vscode' import { getLogger } from '../../shared/logger/logger' import globals from '../../shared/extensionGlobals' -import { Lambda } from 'aws-sdk' -import { getRegionFromArn, isTunnelInfo, LdkClient } from './ldkClient' +import { FunctionConfiguration, Runtime } from '@aws-sdk/client-lambda' +import { getRegionFromArn, LdkClient } from './ldkClient' import { getFamily, mapFamilyToDebugType } from '../models/samLambdaRuntime' import { findJavaPath } from '../../shared/utilities/pathFind' import { ToolkitError } from '../../shared/errors' import { showConfirmationMessage, showMessage } from '../../shared/utilities/messages' import { telemetry } from '../../shared/telemetry/telemetry' import * as nls from 'vscode-nls' -import { getRemoteDebugLayer } from './ldkLayers' import path from 'path' import { glob } from 'glob' import { Commands } from '../../shared/vscode/commands2' +import { getLambdaSnapshot, persistLambdaSnapshot, type LambdaDebugger, type DebugConfig } from './lambdaDebugger' +import { RemoteLambdaDebugger } from './remoteLambdaDebugger' +import { LocalStackLambdaDebugger } from './localStackLambdaDebugger' +import { fs } from '../../shared/fs/fs' +import { detectCdkProjects } from '../../awsService/cdk/explorer/detectCdkProjects' const localize = nls.loadMessageBundle() const logger = getLogger() -export const remoteDebugContextString = 'aws.lambda.remoteDebugContext' -export const remoteDebugSnapshotString = 'aws.lambda.remoteDebugSnapshot' // Map debug types to their corresponding VS Code extension IDs const mapDebugTypeToExtensionId = new Map([ @@ -33,30 +35,10 @@ const mapDebugTypeToExtensionId = new Map([ const mapExtensionToBackup = new Map([['ms-vscode.js-debug', 'ms-vscode.js-debug-nightly']]) -export interface DebugConfig { - functionArn: string - functionName: string - port: number - localRoot: string - remoteRoot: string - skipFiles: string[] - shouldPublishVersion: boolean - lambdaRuntime?: string // Lambda runtime (e.g., nodejs18.x) - debuggerRuntime?: string // VS Code debugger runtime (e.g., node) - outFiles?: string[] - sourceMap?: boolean - justMyCode?: boolean - projectName?: string - otherDebugParams?: string - lambdaTimeout?: number - layerArn?: string - handlerFile?: string -} - // Helper function to create a human-readable diff message function createDiffMessage( - config: Lambda.FunctionConfiguration, - currentConfig: Lambda.FunctionConfiguration, + config: FunctionConfiguration, + currentConfig: FunctionConfiguration, isRevert: boolean = true ): string { let message = isRevert ? 'The following changes will be reverted:\n\n' : 'The following changes will be made:\n\n' @@ -185,18 +167,109 @@ export async function activateRemoteDebugging(): Promise { } } -// this should be called when the debug session is started -async function persistLambdaSnapshot(config: Lambda.FunctionConfiguration | undefined): Promise { +/** + * Try to auto-detect outFile for TypeScript debugging (SAM or CDK) + * @param debugConfig Debug configuration + * @param functionConfig Lambda function configuration + * @returns The auto-detected outFile path or undefined + */ +export async function tryAutoDetectOutFile( + debugConfig: DebugConfig, + functionConfig: FunctionConfiguration +): Promise { + // Only works for TypeScript files + if ( + !debugConfig.handlerFile || + (!debugConfig.handlerFile.endsWith('.ts') && !debugConfig.handlerFile.endsWith('.tsx')) + ) { + return undefined + } + + // Try SAM detection first using the provided parameters + if (debugConfig.samFunctionLogicalId && debugConfig.samProjectRoot) { + // if proj root is ..../sam-proj/ + // build dir will be ..../sam-proj/.aws-sam/build/{LogicalID}/ + const samBuildPath = vscode.Uri.joinPath( + debugConfig.samProjectRoot, + '.aws-sam', + 'build', + debugConfig.samFunctionLogicalId + ) + + if (await fs.exists(samBuildPath)) { + getLogger().info(`SAM outFile auto-detected: ${samBuildPath.fsPath}`) + return samBuildPath.fsPath + } + } + + // If SAM detection didn't work, try CDK detection using the function name + if (!functionConfig.FunctionName) { + return undefined + } + try { - await globals.globalState.update(remoteDebugSnapshotString, config) + // Find which workspace contains the handler file + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(debugConfig.handlerFile)) + if (!workspaceFolder) { + return undefined + } + + // Detect CDK projects in the workspace + const cdkProjects = await detectCdkProjects([workspaceFolder]) + + for (const project of cdkProjects) { + // Check if CDK project contains the handler file + const cdkProjectDir = vscode.Uri.joinPath(project.cdkJsonUri, '..') + // Normalize paths for comparison (handles Windows path separators and case) + const normalizedHandlerPath = path.normalize(debugConfig.handlerFile).toLowerCase() + const normalizedCdkPath = path.normalize(cdkProjectDir.fsPath).toLowerCase() + if (!normalizedHandlerPath.startsWith(normalizedCdkPath)) { + continue + } + + // Get the cdk.out directory + const cdkOutDir = vscode.Uri.joinPath(project.treeUri, '..') + + // Look for template.json files in cdk.out directory + const pattern = new vscode.RelativePattern(cdkOutDir.fsPath, '*.template.json') + const templateFiles = await vscode.workspace.findFiles(pattern) + + for (const templateFile of templateFiles) { + try { + // Read and parse the template.json file + const templateContent = await fs.readFileText(templateFile) + const template = JSON.parse(templateContent) + + // Search through resources for a Lambda function with matching FunctionName + for (const [_, resource] of Object.entries(template.Resources || {})) { + const res = resource as any + if ( + res.Type === 'AWS::Lambda::Function' && + res.Properties?.FunctionName === functionConfig.FunctionName + ) { + // Found the matching function, extract the asset path from metadata + const assetPath = res.Metadata?.['aws:asset:path'] + if (assetPath) { + const assetDir = vscode.Uri.joinPath(cdkOutDir, assetPath) + + // Check if the asset directory exists + if (await fs.exists(assetDir)) { + getLogger().info(`CDK outFile auto-detected from template.json: ${assetDir.fsPath}`) + return assetDir.fsPath + } + } + } + } + } catch (error) { + getLogger().debug(`Failed to parse template file ${templateFile.fsPath}: ${error}`) + } + } + } } catch (error) { - // TODO raise toolkit error - logger.error(`Error persisting debug sessions:${error}`) + getLogger().warn(`Failed to auto-detect CDK outFile: ${error}`) } -} -export function getLambdaSnapshot(): Lambda.FunctionConfiguration | undefined { - return globals.globalState.get(remoteDebugSnapshotString) + return undefined } /** @@ -207,19 +280,65 @@ function isVscodeGlob(pattern: string): boolean { return /[*?[\]{}]/.test(pattern) } +/** + * Extract temp directory patterns from source map files + * @param mapFiles Array of source map file paths + * @returns Set of temp directory patterns found in source maps + */ +async function extractTempPatternsFromSourceMaps(mapFiles: string[]): Promise> { + const tempPatterns = new Set() + + for (const mapFile of mapFiles) { + try { + const content = await fs.readFileText(mapFile) + const sourceMap = JSON.parse(content) + + if (sourceMap.sources && Array.isArray(sourceMap.sources)) { + for (const source of sourceMap.sources) { + // SAM uses Python's tempfile.mkdtemp() to create tmp dir which we want to detect + // tempfile.mkdtemp() uses lowercase letters, digits, and underscores + // The pattern is: tmp followed by 8 characters from [a-z0-9_] + // see https://github.com/python/cpython/blob/20a677d75a95fa63be904f7ca4f8cb268aec95c1/Lib/tempfile.py#L132-L140 + const tempMatch = source.match(/\btmp[a-z0-9_]{8}\b/) + if (tempMatch) { + tempPatterns.add(tempMatch[0]) + getLogger().debug(`Found temp pattern in source map: ${tempMatch[0]}`) + } + } + } + } catch (error) { + getLogger().debug(`Failed to read or parse source map ${mapFile}: ${error}`) + } + } + + return tempPatterns +} + /** * Helper function to validate source map files exist for given outFiles patterns + * @returns Object with validation result and temp patterns found in source maps */ -async function validateSourceMapFiles(outFiles: string[]): Promise { +export async function validateSourceMapFiles( + outFiles: string[] +): Promise<{ isValid: boolean; tempPatterns: Set }> { + getLogger().debug(`validating outFiles ${outFiles}`) const allAreGlobs = outFiles.every((pattern) => isVscodeGlob(pattern)) if (!allAreGlobs) { - return false + return { isValid: false, tempPatterns: new Set() } } try { let jsfileCount = 0 let mapfileCount = 0 - const jsFiles = await glob(outFiles, { ignore: 'node_modules/**' }) + const mapFiles: string[] = [] + + // Convert Windows paths to use forward slashes for glob + const normalizedOutFiles = outFiles.map((pattern) => { + // Replace backslashes with forward slashes for glob compatibility + return pattern.replaceAll(/\\/g, '/') + }) + getLogger().debug(`normalizedOutFiles ${normalizedOutFiles}`) + const jsFiles = await glob(normalizedOutFiles, { ignore: 'node_modules/**' }) for (const file of jsFiles) { if (file.includes('js')) { @@ -227,13 +346,20 @@ async function validateSourceMapFiles(outFiles: string[]): Promise { } if (file.includes('.map')) { mapfileCount += 1 + mapFiles.push(file) } } - return jsfileCount === 0 || mapfileCount === 0 ? false : true + // Extract temp patterns from source map files + const tempPatterns = await extractTempPatternsFromSourceMaps(mapFiles) + + return { + isValid: jsfileCount > 0 && mapfileCount > 0, + tempPatterns, + } } catch (error) { getLogger().warn(`Error validating source map files: ${error}`) - return false + return { isValid: false, tempPatterns: new Set() } } } @@ -282,7 +408,7 @@ function processOutFiles(outFiles: string[], localRoot: string): string[] { } async function getVscodeDebugConfig( - functionConfig: Lambda.FunctionConfiguration, + functionConfig: FunctionConfiguration, debugConfig: DebugConfig ): Promise { // Parse and validate otherDebugParams if provided @@ -317,10 +443,19 @@ async function getVscodeDebugConfig( const debugSessionName = `Debug ${functionConfig.FunctionArn!.split(':').pop()}` // Define debugConfig before the try block - const debugType = mapFamilyToDebugType.get(getFamily(functionConfig.Runtime ?? ''), 'unknown') + const debugType = mapFamilyToDebugType.get(getFamily(functionConfig.Runtime!), 'unknown') let vsCodeDebugConfig: vscode.DebugConfiguration switch (debugType) { case 'node': + // Try to auto-detect outFiles for TypeScript if not provided + if (debugConfig.sourceMap && !debugConfig.outFiles && debugConfig.handlerFile) { + const autoDetectedOutFile = await tryAutoDetectOutFile(debugConfig, functionConfig) + if (autoDetectedOutFile) { + debugConfig.outFiles = [autoDetectedOutFile] + getLogger().info(`outFile auto-detected: ${autoDetectedOutFile}`) + } + } + // source map support if (debugConfig.sourceMap && debugConfig.outFiles) { // process outFiles first, if they are relative path (not starting with /), @@ -330,19 +465,26 @@ async function getVscodeDebugConfig( debugConfig.outFiles = processOutFiles(debugConfig.outFiles, debugConfig.localRoot) // Use glob to search if there are any matching js file or source map file - const hasSourceMaps = await validateSourceMapFiles(debugConfig.outFiles) + const sourceMapValidation = await validateSourceMapFiles(debugConfig.outFiles) - if (hasSourceMaps) { - // support mapping common sam cli location - additionalParams['sourceMapPathOverrides'] = { + if (sourceMapValidation.isValid) { + // Start with basic source map overrides + const sourceMapOverrides: Record = { ...additionalParams['sourceMapPathOverrides'], - '?:*/T/?:*/*': path.join(debugConfig.localRoot, '*'), } + + // Add specific temp directory patterns found in source maps + for (const tempPattern of sourceMapValidation.tempPatterns) { + sourceMapOverrides[`?:*/${tempPattern}/*`] = path.join(debugConfig.localRoot, '*') + getLogger().info(`Added source map override for temp pattern: ${tempPattern}`) + } + + additionalParams['sourceMapPathOverrides'] = sourceMapOverrides debugConfig.localRoot = debugConfig.outFiles[0].split('*')[0] } else { debugConfig.sourceMap = false debugConfig.outFiles = undefined - await showMessage( + void showMessage( 'warn', localize( 'AWS.lambda.remoteDebug.outFileNotFound', @@ -409,9 +551,11 @@ export class RemoteDebugController { static #instance: RemoteDebugController isDebugging: boolean = false qualifier: string | undefined = undefined + debugger: LambdaDebugger | undefined = undefined private lastDebugStartTime: number = 0 // private debugSession: DebugSession | undefined private debugSessionDisposables: Map = new Map() + private debugTypeSource: 'remoteDebug' | 'LocalStackDebug' = 'remoteDebug' public static get instance() { if (this.#instance !== undefined) { @@ -442,10 +586,14 @@ export class RemoteDebugController { } } - public supportCodeDownload(runtime: string | undefined): boolean { + public supportCodeDownload(runtime: Runtime | undefined, codeSha256: string | undefined = ''): boolean { if (!runtime) { return false } + // Incompatible with LocalStack hot-reloading + if (codeSha256?.startsWith('hot-reloading')) { + return false + } try { return ['node', 'python'].includes(mapFamilyToDebugType.get(getFamily(runtime)) ?? '') } catch { @@ -454,7 +602,7 @@ export class RemoteDebugController { } } - public supportRuntimeRemoteDebug(runtime: string | undefined): boolean { + public supportRuntimeRemoteDebug(runtime: Runtime | undefined): boolean { if (!runtime) { return false } @@ -465,23 +613,7 @@ export class RemoteDebugController { } } - public getRemoteDebugLayer( - region: string | undefined, - architectures: Lambda.ArchitecturesList | undefined - ): string | undefined { - if (!region || !architectures) { - return undefined - } - if (architectures.includes('x86_64')) { - return getRemoteDebugLayer(region, 'x86_64') - } - if (architectures.includes('arm64')) { - return getRemoteDebugLayer(region, 'arm64') - } - return undefined - } - - public async installDebugExtension(runtime: string | undefined): Promise { + public async installDebugExtension(runtime: Runtime | undefined): Promise { if (!runtime) { throw new ToolkitError('Runtime is undefined') } @@ -545,6 +677,20 @@ export class RemoteDebugController { } public async startDebugging(functionArn: string, runtime: string, debugConfig: DebugConfig): Promise { + if (debugConfig.isLambdaRemote) { + this.debugTypeSource = 'remoteDebug' + this.debugger = new RemoteLambdaDebugger(debugConfig, { + getQualifier: () => { + return this.qualifier + }, + setQualifier: (qualifier) => { + this.qualifier = qualifier + }, + }) + } else { + this.debugTypeSource = 'LocalStackDebug' + this.debugger = new LocalStackLambdaDebugger(debugConfig) + } if (this.isDebugging) { getLogger().error('Debug already in progress, remove debug setup to restart') return @@ -558,7 +704,7 @@ export class RemoteDebugController { debugConfigForTelemetry.localRoot = undefined span.record({ - source: 'remoteDebug', + source: this.debugTypeSource, passive: false, action: JSON.stringify(debugConfigForTelemetry), }) @@ -581,13 +727,16 @@ export class RemoteDebugController { } // Check if runtime / region is supported for remote debugging - if (!this.supportRuntimeRemoteDebug(runtime)) { + if (!this.supportRuntimeRemoteDebug(runtime as Runtime)) { throw new ToolkitError( `Runtime ${runtime} is not supported for remote debugging. ` + `Only Python, Node.js, and Java runtimes are supported.` ) } + // Ensure the remote connection is reachable before calling lambda.GetFunction in revertExistingConfig() + await this.debugger?.checkHealth() + // Check if a snapshot already exists and revert if needed // Use the revertExistingConfig function from ldkController progress.report({ message: 'Checking if snapshot exists...' }) @@ -606,7 +755,6 @@ export class RemoteDebugController { // let's preserve this config to a global variable at here // we will use this config to revert the changes back to it once was, once confirm it's success, update the global to undefined // if somehow the changes failed to revert, in init phase(activate remote debugging), we will detect this config and prompt user to revert the changes - const ldkClient = LdkClient.instance // get function config again in case anything changed const functionConfig = await LdkClient.instance.getFunctionDetail(functionArn) if (!functionConfig?.Runtime || !functionConfig?.FunctionArn) { @@ -619,56 +767,14 @@ export class RemoteDebugController { runtimeString: functionConfig.Runtime as any, }) - // Create or reuse tunnel - progress.report({ message: 'Creating secure tunnel...' }) - getLogger().info('Creating secure tunnel...') - const tunnelInfo = await ldkClient.createOrReuseTunnel(region) - if (!tunnelInfo) { - throw new ToolkitError(`Empty tunnel info response, please retry:${tunnelInfo}`) - } - - if (!isTunnelInfo(tunnelInfo)) { - throw new ToolkitError(`Invalid tunnel info response:${tunnelInfo}`) - } - // start update lambda funcion, await in the end - // Create debug deployment - progress.report({ message: 'Configuring Lambda function for debugging...' }) - getLogger().info('Configuring Lambda function for debugging...') - - const layerArn = - debugConfig.layerArn ?? this.getRemoteDebugLayer(region, functionConfig.Architectures) - if (!layerArn) { - throw new ToolkitError(`No Layer Arn is provided`) - } - // start this request and await in the end - const debugDeployPromise = ldkClient.createDebugDeployment( - functionConfig, - tunnelInfo.destinationToken, - debugConfig.lambdaTimeout ?? 900, - debugConfig.shouldPublishVersion, - layerArn, - progress - ) + await this.debugger?.setup(progress, functionConfig, region) const vscodeDebugConfig = await getVscodeDebugConfig(functionConfig, debugConfig) // show every field in debugConfig // getLogger().info(`Debug configuration created successfully ${JSON.stringify(debugConfig)}`) - // Start local proxy with timeout and better error handling - progress.report({ message: 'Starting local proxy...' }) - - const proxyStartTimeout = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Local proxy start timed out')), 30000) - }) - - const proxyStartAttempt = ldkClient.startProxy(region, tunnelInfo.sourceToken, debugConfig.port) - - const proxyStarted = await Promise.race([proxyStartAttempt, proxyStartTimeout]) + await this.debugger?.waitForSetup(progress, functionConfig, region) - if (!proxyStarted) { - throw new ToolkitError('Failed to start local proxy') - } - getLogger().info('Local proxy started successfully') progress.report({ message: 'Starting debugger...' }) // Start debugging in a non-blocking way void Promise.resolve(vscode.debug.startDebugging(undefined, vscodeDebugConfig)).then( @@ -686,17 +792,7 @@ export class RemoteDebugController { } }) - // wait until lambda function update is completed - progress.report({ message: 'Waiting for function update...' }) - const qualifier = await debugDeployPromise - if (!qualifier || qualifier === 'Failed') { - throw new ToolkitError('Failed to configure Lambda function for debugging') - } - // store the published version for debugging in version - if (debugConfig.shouldPublishVersion) { - // we already reverted - this.qualifier = qualifier - } + await this.debugger?.waitForFunctionUpdates(progress) // Store the disposable this.debugSessionDisposables.set(functionConfig.FunctionArn, debugSessionEndDisposable) @@ -708,7 +804,7 @@ export class RemoteDebugController { await this.stopDebugging() } catch (errStop) { getLogger().error( - 'encountered following error when stoping debug for failed debug session:' + 'encountered following error when stopping debug for failed debug session:' ) getLogger().error(errStop as Error) } @@ -730,7 +826,10 @@ export class RemoteDebugController { return } // use sessionDuration to record debug duration - span.record({ sessionDuration: this.lastDebugStartTime === 0 ? 0 : Date.now() - this.lastDebugStartTime }) + span.record({ + sessionDuration: this.lastDebugStartTime === 0 ? 0 : Date.now() - this.lastDebugStartTime, + source: this.debugTypeSource, + }) try { await vscode.window.withProgress( { @@ -740,7 +839,6 @@ export class RemoteDebugController { }, async (progress) => { progress.report({ message: 'Stopping debugging...' }) - const ldkClient = LdkClient.instance // First attempt to clean up resources from Lambda const savedConfig = getLambdaSnapshot() @@ -754,19 +852,7 @@ export class RemoteDebugController { disposable.dispose() this.debugSessionDisposables.delete(savedConfig.FunctionArn) } - getLogger().info(`Removing debug deployment for function: ${savedConfig.FunctionName}`) - - await vscode.commands.executeCommand('workbench.action.debug.stop') - // Then stop the proxy (with more reliable error handling) - getLogger().info('Stopping proxy during cleanup') - await ldkClient.stopProxy() - // Ensure our resources are properly cleaned up - if (this.qualifier) { - await ldkClient.deleteDebugVersion(savedConfig.FunctionArn, this.qualifier) - } - if (await ldkClient.removeDebugDeployment(savedConfig, true)) { - await persistLambdaSnapshot(undefined) - } + await this.debugger?.cleanup(savedConfig) progress.report({ message: `Debug session stopped` }) } diff --git a/packages/core/src/lambda/remoteDebugging/ldkLayers.ts b/packages/core/src/lambda/remoteDebugging/ldkLayers.ts index 5573a84f980..cf955a6c019 100644 --- a/packages/core/src/lambda/remoteDebugging/ldkLayers.ts +++ b/packages/core/src/lambda/remoteDebugging/ldkLayers.ts @@ -31,9 +31,9 @@ export const regionToAccount: RegionAccountMapping = { } // Global layer version -const globalLayerVersion = 1 +const globalLayerVersion = 3 -export function getRemoteDebugLayer(region: string, arch: string): string | undefined { +export function getRemoteDebugLayerForArch(region: string, arch: string): string | undefined { const account = regionToAccount[region] if (!account) { diff --git a/packages/core/src/lambda/remoteDebugging/localStackLambdaDebugger.ts b/packages/core/src/lambda/remoteDebugging/localStackLambdaDebugger.ts new file mode 100644 index 00000000000..d74b6ac3471 --- /dev/null +++ b/packages/core/src/lambda/remoteDebugging/localStackLambdaDebugger.ts @@ -0,0 +1,164 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' +import globals from '../../shared/extensionGlobals' +import { persistLambdaSnapshot, type LambdaDebugger, type DebugConfig } from './lambdaDebugger' +import { getLambdaClientWithAgent, getLambdaDebugUserAgent } from './utils' +import { getLogger } from '../../shared/logger/logger' +import { ToolkitError } from '../../shared/errors' + +export class LocalStackLambdaDebugger implements LambdaDebugger { + private debugConfig: DebugConfig + + constructor(debugConfig: DebugConfig) { + this.debugConfig = debugConfig + } + + public async checkHealth(): Promise { + const endpointUrl = globals.awsContext.getCredentialEndpointUrl() + const localStackHealthUrl = `${endpointUrl}/_localstack/health` + const localStackNotRunningMessage = 'LocalStack is not reachable. Ensure LocalStack is running!' + try { + const response = await fetch(localStackHealthUrl) + if (!response.ok) { + getLogger().error(`LocalStack health check failed with status ${response.status}`) + throw new ToolkitError(localStackNotRunningMessage) + } + } catch (error) { + throw ToolkitError.chain(error, localStackNotRunningMessage) + } + } + + public async setup( + progress: vscode.Progress<{ message?: string; increment?: number }>, + functionConfig: FunctionConfiguration, + region: string + ): Promise { + // No function update and version publishing needed for LocalStack + this.debugConfig.shouldPublishVersion = false + + progress.report({ message: 'Creating LocalStack debug configuration...' }) + const endpointUrl = globals.awsContext.getCredentialEndpointUrl() + const localStackLDMUrl = `${endpointUrl}/_aws/lambda/debug_configs/${functionConfig.FunctionArn}:$LATEST` + const response = await fetch(localStackLDMUrl, { + method: 'PUT', + body: JSON.stringify({ + port: this.debugConfig.port, + user_agent: getLambdaDebugUserAgent(), + }), + }) + + if (!response.ok) { + const error = await this.errorFromResponse(response) + if (error.startsWith('UnsupportedLocalStackVersion')) { + void vscode.window.showErrorMessage(`${error}`, 'Update LocalStack Docker image').then((selection) => { + if (selection) { + const terminal = vscode.window.createTerminal('Update LocalStack Docker image') + terminal.show() + terminal.sendText('localstack update docker-images') + } + }) + } else { + void vscode.window.showErrorMessage(error) + } + + throw ToolkitError.chain( + error, + `Failed to create LocalStack debug configuration for Lambda function ${functionConfig.FunctionName}.` + ) + } + + const json = await response.json() + this.debugConfig.port = json.port + } + + private async errorFromResponse(response: Response): Promise { + const isXml = response.headers.get('content-type') === 'application/xml' + if (isXml) { + return 'UnsupportedLocalStackVersion: Your current LocalStack version does not support Lambda remote debugging. Update LocalStack and check your license.' + } + + const isJson = response.headers.get('content-type') === 'application/json' + if (isJson) { + const json = await response.json() + if (json.error.type !== undefined && json.error.message !== undefined) { + return `${json.error.type}: ${json.error.message}` + } + } + + return 'Unknown error' + } + + public async waitForSetup( + progress: vscode.Progress<{ message?: string; increment?: number }>, + functionConfig: FunctionConfiguration, + region: string + ): Promise { + if (!functionConfig?.FunctionArn) { + throw new ToolkitError('Could not retrieve Lambda function configuration') + } + + progress.report({ message: 'Waiting for Lambda function to become Active...' }) + getLogger().info(`Waiting for ${functionConfig.FunctionArn} to become Active...`) + try { + await getLambdaClientWithAgent(region).waitForActive(functionConfig.FunctionArn) + } catch (error) { + throw ToolkitError.chain(error, 'Lambda function failed to become Active.') + } + + progress.report({ message: 'Waiting for startup of execution environment and debugger...' }) + getLogger().info(`Waiting for ${functionConfig.FunctionArn} to startup execution environment and debugger...`) + const endpointUrl = globals.awsContext.getCredentialEndpointUrl() + const localStackLDMUrl = `${endpointUrl}/_aws/lambda/debug_configs/${functionConfig.FunctionArn}:$LATEST?debug_server_ready_timeout=300` + // Blocking call to wait for the Lambda function debug server to be running. LocalStack probes the debug server. + const response = await fetch(localStackLDMUrl, { method: 'GET' }) + if (!response.ok) { + const error = await this.errorFromResponse(response) + throw ToolkitError.chain( + new Error(error), + `Failed to startup execution environment or debugger for Lambda function ${functionConfig.FunctionName}.` + ) + } + + const json = await response.json() + if (json.is_debug_server_running !== true) { + throw new ToolkitError( + `Debug server on port ${this.debugConfig.port} is not running for Lambda function ${functionConfig.FunctionName}.` + ) + } + + getLogger().info(`${functionConfig.FunctionArn} is ready for debugging on port ${this.debugConfig.port}.`) + } + + public async waitForFunctionUpdates( + progress: vscode.Progress<{ message?: string; increment?: number }> + ): Promise { + // No additional steps needed for LocalStack: + // a) Port probing ensures the debug server is ready + // b) Invokes for debug-enabled await being served until the debugger is connected + } + + public async cleanup(functionConfig: FunctionConfiguration): Promise { + await vscode.commands.executeCommand('workbench.action.debug.stop') + + const endpointUrl = globals.awsContext.getCredentialEndpointUrl() + const localStackLDMUrl = `${endpointUrl}/_aws/lambda/debug_configs/${functionConfig.FunctionArn}:$LATEST` + const response = await fetch(localStackLDMUrl, { method: 'DELETE' }) + if (!response.ok) { + const error = await this.errorFromResponse(response) + getLogger().warn( + `Failed to remove LocalStack debug configuration for ${functionConfig.FunctionArn}. ${error}` + ) + throw new ToolkitError( + `Failed to remove LocalStack debug configuration for Lambda function ${functionConfig.FunctionName}.` + ) + } + + await persistLambdaSnapshot(undefined) + getLogger().info(`Removed LocalStack debug configuration for ${functionConfig.FunctionArn}`) + } +} diff --git a/packages/core/src/lambda/remoteDebugging/remoteLambdaDebugger.ts b/packages/core/src/lambda/remoteDebugging/remoteLambdaDebugger.ts new file mode 100644 index 00000000000..afc9f83abcd --- /dev/null +++ b/packages/core/src/lambda/remoteDebugging/remoteLambdaDebugger.ts @@ -0,0 +1,155 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { Architecture, FunctionConfiguration } from '@aws-sdk/client-lambda' +import { persistLambdaSnapshot, type LambdaDebugger, type DebugConfig } from './lambdaDebugger' +import { getLogger } from '../../shared/logger/logger' +import { isTunnelInfo, LdkClient } from './ldkClient' +import type { TunnelInfo } from './ldkClient' +import { ToolkitError } from '../../shared/errors' +import { getRemoteDebugLayerForArch } from './ldkLayers' + +export function getRemoteDebugLayer( + region: string | undefined, + architectures: Architecture[] | undefined +): string | undefined { + if (!region || !architectures) { + return undefined + } + if (architectures.includes('x86_64')) { + return getRemoteDebugLayerForArch(region, 'x86_64') + } + if (architectures.includes('arm64')) { + return getRemoteDebugLayerForArch(region, 'arm64') + } + return undefined +} + +export interface QualifierProxy { + setQualifier(qualifier: string): void + getQualifier(): string | undefined +} + +export class RemoteLambdaDebugger implements LambdaDebugger { + private debugConfig: DebugConfig + private debugDeployPromise: Promise | undefined + private tunnelInfo: TunnelInfo | undefined + private qualifierProxy: QualifierProxy + + constructor(debugConfig: DebugConfig, qualifierProxy: QualifierProxy) { + this.debugConfig = debugConfig + this.qualifierProxy = qualifierProxy + } + + public async checkHealth(): Promise { + // We assume AWS is always available + } + + public async setup( + progress: vscode.Progress<{ message?: string; increment?: number }>, + functionConfig: FunctionConfiguration, + region: string + ): Promise { + const ldkClient = LdkClient.instance + // Create or reuse tunnel + progress.report({ message: 'Creating secure tunnel...' }) + getLogger().info('Creating secure tunnel...') + this.tunnelInfo = await ldkClient.createOrReuseTunnel(region) + if (!this.tunnelInfo) { + throw new ToolkitError(`Empty tunnel info response, please retry: ${this.tunnelInfo}`) + } + + if (!isTunnelInfo(this.tunnelInfo)) { + throw new ToolkitError(`Invalid tunnel info response: ${this.tunnelInfo}`) + } + // start update lambda function, await in the end + // Create debug deployment + progress.report({ message: 'Configuring Lambda function for debugging...' }) + getLogger().info('Configuring Lambda function for debugging...') + + const layerArn = this.debugConfig.layerArn ?? getRemoteDebugLayer(region, functionConfig.Architectures) + if (!layerArn) { + throw new ToolkitError(`No Layer Arn is provided`) + } + // start this request and await in the end + this.debugDeployPromise = ldkClient.createDebugDeployment( + functionConfig, + this.tunnelInfo.destinationToken, + this.debugConfig.lambdaTimeout ?? 900, + this.debugConfig.shouldPublishVersion, + layerArn, + progress + ) + } + + public async waitForSetup( + progress: vscode.Progress<{ message?: string; increment?: number }>, + functionConfig: FunctionConfiguration, + region: string + ): Promise { + if (!this.tunnelInfo) { + throw new ToolkitError(`Empty tunnel info response, please retry: ${this.tunnelInfo}`) + } + + // Start local proxy with timeout and better error handling + progress.report({ message: 'Starting local proxy...' }) + + const proxyStartTimeout = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Local proxy start timed out')), 30000) + }) + + const proxyStartAttempt = LdkClient.instance.startProxy( + region, + this.tunnelInfo.sourceToken, + this.debugConfig.port + ) + + const proxyStarted = await Promise.race([proxyStartAttempt, proxyStartTimeout]) + + if (!proxyStarted) { + throw new ToolkitError('Failed to start local proxy') + } + getLogger().info('Local proxy started successfully') + } + + public async waitForFunctionUpdates( + progress: vscode.Progress<{ message?: string; increment?: number }> + ): Promise { + // wait until lambda function update is completed + progress.report({ message: 'Waiting for function update...' }) + const qualifier = await this.debugDeployPromise + if (!qualifier || qualifier === 'Failed') { + throw new ToolkitError('Failed to configure Lambda function for debugging') + } + // store the published version for debugging in version + if (this.debugConfig.shouldPublishVersion) { + // we already reverted + this.qualifierProxy.setQualifier(qualifier) + } + } + + public async cleanup(functionConfig: FunctionConfiguration): Promise { + const ldkClient = LdkClient.instance + if (!functionConfig?.FunctionArn) { + throw new ToolkitError('No saved configuration found during cleanup') + } + + getLogger().info(`Removing debug deployment for function: ${functionConfig.FunctionName}`) + + await vscode.commands.executeCommand('workbench.action.debug.stop') + // Then stop the proxy (with more reliable error handling) + getLogger().info('Stopping proxy during cleanup') + await ldkClient.stopProxy() + // Ensure our resources are properly cleaned up + const qualifier = this.qualifierProxy.getQualifier() + if (qualifier) { + await ldkClient.deleteDebugVersion(functionConfig?.FunctionArn, qualifier) + } + if (await ldkClient.removeDebugDeployment(functionConfig, true)) { + await persistLambdaSnapshot(undefined) + } + } +} diff --git a/packages/core/src/lambda/remoteDebugging/utils.ts b/packages/core/src/lambda/remoteDebugging/utils.ts index 6f7256f9f61..fade27c21c7 100644 --- a/packages/core/src/lambda/remoteDebugging/utils.ts +++ b/packages/core/src/lambda/remoteDebugging/utils.ts @@ -3,25 +3,54 @@ * SPDX-License-Identifier: Apache-2.0 */ -import IoTSecureTunneling from 'aws-sdk/clients/iotsecuretunneling' +import { IoTSecureTunnelingClient } from '@aws-sdk/client-iotsecuretunneling' import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' -import { getUserAgent } from '../../shared/telemetry/util' +import { getUserAgentPairs, userAgentPairsToString } from '../../shared/telemetry/util' import globals from '../../shared/extensionGlobals' +import type { UserAgent } from '@aws-sdk/types' -const customUserAgentBase = 'LAMBDA-DEBUG/1.0.0' +const customUserAgentName = 'LAMBDA-DEBUG' +const customUserAgentVersion = '1.0.0' -export function getLambdaClientWithAgent(region: string): DefaultLambdaClient { - const customUserAgent = `${customUserAgentBase} ${getUserAgent({ includePlatform: true, includeClientId: true })}` +export function getLambdaClientWithAgent(region: string, customUserAgent?: UserAgent): DefaultLambdaClient { + if (!customUserAgent) { + customUserAgent = getLambdaUserAgentPairs() + } return new DefaultLambdaClient(region, customUserAgent) } -export function getIoTSTClientWithAgent(region: string): Promise { - const customUserAgent = `${customUserAgentBase} ${getUserAgent({ includePlatform: true, includeClientId: true })}` - return globals.sdkClientBuilder.createAwsService( - IoTSecureTunneling, - { - customUserAgent, +/** + * Returns properly formatted UserAgent pairs for AWS SDK v3 + */ +export function getLambdaDebugUserAgentPairs(): UserAgent { + return [ + [customUserAgentName, customUserAgentVersion], + ...getUserAgentPairs({ includePlatform: true, includeClientId: true }), + ] +} + +/** + * Returns properly formatted UserAgent pairs for AWS SDK v3 + */ +export function getLambdaUserAgentPairs(): UserAgent { + return getUserAgentPairs({ includePlatform: true, includeClientId: true }) +} + +/** + * Returns user agent string for Lambda debugging in traditional format. + * Example: "LAMBDA-DEBUG/1.0.0 AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.105.1 ClientId/11111111-1111-1111-1111-111111111111" + */ +export function getLambdaDebugUserAgent(): string { + return userAgentPairsToString(getLambdaDebugUserAgentPairs()) +} + +export function getIoTSTClientWithAgent(region: string): IoTSecureTunnelingClient { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: IoTSecureTunnelingClient, + clientOptions: { + customUserAgent: getLambdaDebugUserAgentPairs(), + region, }, - region - ) + userAgent: false, + }) } diff --git a/packages/core/src/lambda/utils.ts b/packages/core/src/lambda/utils.ts index eeea6451342..9b8ffcb884a 100644 --- a/packages/core/src/lambda/utils.ts +++ b/packages/core/src/lambda/utils.ts @@ -8,7 +8,7 @@ const localize = nls.loadMessageBundle() import path from 'path' import xml2js = require('xml2js') -import { Lambda } from 'aws-sdk' +import { FunctionConfiguration, LayerVersionsListItem } from '@aws-sdk/client-lambda' import * as vscode from 'vscode' import { CloudFormationClient, StackSummary } from '../shared/clients/cloudFormation' import { DefaultLambdaClient, LambdaClient } from '../shared/clients/lambdaClient' @@ -36,7 +36,7 @@ export async function* listCloudFormationStacks(client: CloudFormationClient): A } } -export async function* listLambdaFunctions(client: LambdaClient): AsyncIterableIterator { +export async function* listLambdaFunctions(client: LambdaClient): AsyncIterableIterator { const status = vscode.window.setStatusBarMessage( localize('AWS.message.statusBar.loading.lambda', 'Loading Lambdas...') ) @@ -53,7 +53,7 @@ export async function* listLambdaFunctions(client: LambdaClient): AsyncIterableI export async function* listLayerVersions( client: LambdaClient, name: string -): AsyncIterableIterator { +): AsyncIterableIterator { const status = vscode.window.setStatusBarMessage( localize('AWS.message.statusBar.loading.lambda', 'Loading Lambda Layer Versions...') ) @@ -72,7 +72,7 @@ export async function* listLayerVersions( * Only works for supported languages (Python/JS) * @param configuration Lambda configuration object from getFunction */ -export function getLambdaDetails(configuration: Lambda.FunctionConfiguration): { +export function getLambdaDetails(configuration: FunctionConfiguration): { fileName: string functionName: string } { @@ -207,3 +207,8 @@ export function getTempRegionLocation(region: string) { export function getTempLocation(functionName: string, region: string) { return path.join(getTempRegionLocation(region), functionName) } + +// LocalStack hot-reloading: https://docs.localstack.cloud/aws/tooling/lambda-tools/hot-reloading/ +export function isHotReloadingFunction(codeSha256: string | undefined): boolean { + return codeSha256?.startsWith('hot-reloading') ?? false +} diff --git a/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts b/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts index 2084ebe82fe..2f00d8a4373 100644 --- a/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts +++ b/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts @@ -28,6 +28,7 @@ import * as CloudFormation from '../../../shared/cloudformation/cloudformation' import { openLaunchJsonFile } from '../../../shared/sam/debugger/commands/addSamDebugConfiguration' import { getSampleLambdaPayloads } from '../../utils' import { samLambdaCreatableRuntimes } from '../../models/samLambdaRuntime' +import { isFunctionResource } from '../../../awsService/appBuilder/explorer/samProject' import globals from '../../../shared/extensionGlobals' import { VueWebview } from '../../../webviews/main' import { Commands } from '../../../shared/vscode/commands2' @@ -441,6 +442,10 @@ export async function registerSamDebugInvokeVueCommand( (config) => (config.invokeTarget as TemplateTargetProperties).logicalId === resource.resource.Id ) + if (!isFunctionResource(resource.resource)) { + throw new ToolkitError('Resource is not a Lambda function') + } + const webview = new WebviewPanel(context, launchConfig, { logicalId: resource.resource.Id ?? '', region: resource.region ?? '', diff --git a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts index 9e027bde2bc..bb464250fe6 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts +++ b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { _Blob } from 'aws-sdk/clients/lambda' import { readFileSync } from 'fs' // eslint-disable-line no-restricted-imports import * as _ from 'lodash' import * as vscode from 'vscode' @@ -15,11 +14,12 @@ import { getLogger } from '../../../shared/logger/logger' import { HttpResourceFetcher } from '../../../shared/resourcefetcher/httpResourceFetcher' import { sampleRequestPath } from '../../constants' import { LambdaFunctionNode } from '../../explorer/lambdaFunctionNode' -import { getSampleLambdaPayloads, SampleRequest } from '../../utils' +import { getSampleLambdaPayloads, SampleRequest, isHotReloadingFunction } from '../../utils' import * as nls from 'vscode-nls' import { VueWebview } from '../../../webviews/main' -import { telemetry, Result, Runtime } from '../../../shared/telemetry/telemetry' +import { telemetry, Runtime as TelemetryRuntime } from '../../../shared/telemetry/telemetry' +import { Runtime } from '@aws-sdk/client-lambda' import { runSamCliRemoteTestEvents, SamCliRemoteTestEventsParameters, @@ -29,13 +29,16 @@ import { getSamCliContext } from '../../../shared/sam/cli/samCliContext' import { ToolkitError } from '../../../shared/errors' import { basename } from 'path' import { decodeBase64 } from '../../../shared/utilities/textUtilities' -import { DebugConfig, RemoteDebugController, revertExistingConfig } from '../../remoteDebugging/ldkController' +import { RemoteDebugController, revertExistingConfig } from '../../remoteDebugging/ldkController' +import type { DebugConfig } from '../../remoteDebugging/lambdaDebugger' import { getCachedLocalPath, openLambdaFile, runDownloadLambda } from '../../commands/downloadLambda' import { getLambdaHandlerFile } from '../../../awsService/appBuilder/utils' import { runUploadDirectory } from '../../commands/uploadLambda' import fs from '../../../shared/fs/fs' import { showConfirmationMessage, showMessage } from '../../../shared/utilities/messages' -import { getLambdaClientWithAgent } from '../../remoteDebugging/utils' +import { getLambdaClientWithAgent, getLambdaDebugUserAgentPairs } from '../../remoteDebugging/utils' +import { isLocalStackConnection } from '../../../auth/utils' +import { getRemoteDebugLayer } from '../../remoteDebugging/remoteLambdaDebugger' const localize = nls.loadMessageBundle() @@ -55,17 +58,18 @@ export interface InitialData { Source?: string StackName?: string LogicalId?: string - Runtime?: string + Runtime?: Runtime LocalRootPath?: string LambdaFunctionNode?: LambdaFunctionNode supportCodeDownload?: boolean runtimeSupportsRemoteDebug?: boolean remoteDebugLayer?: string | undefined + isLambdaRemote?: boolean } // Debug configuration sub-interface export interface DebugConfiguration { - debugPort: number + debugPort: number | undefined localRootPath: string remoteRootPath: string shouldPublishVersion: boolean @@ -98,18 +102,12 @@ export interface RuntimeDebugSettings { // UI state sub-interface export interface UIState { isCollapsed: boolean - showNameInput: boolean - payload: string + extraRegionInfo: string } // Payload/Event handling sub-interface export interface PayloadData { - selectedSampleRequest: string sampleText: string - selectedFile: string - selectedFilePath: string - selectedTestEvent: string - newTestEventName: string } export interface RemoteInvokeData { @@ -149,6 +147,7 @@ export class RemoteInvokeWebview extends VueWebview { public constructor( private readonly channel: vscode.OutputChannel, private readonly client: LambdaClient, + private readonly clientDebug: LambdaClient, private readonly data: InitialData ) { super(RemoteInvokeWebview.sourcePath) @@ -266,7 +265,6 @@ export class RemoteInvokeWebview extends VueWebview { } public async invokeLambda(input: string, source?: string, remoteDebugEnabled: boolean = false): Promise { - let result: Result = 'Succeeded' let qualifier: string | undefined = undefined // if debugging, focus on the first editor if (remoteDebugEnabled && RemoteDebugController.instance.isDebugging) { @@ -283,43 +281,58 @@ export class RemoteInvokeWebview extends VueWebview { this.channel.show() this.channel.appendLine('Loading response...') + await telemetry.lambda_invokeRemote.run(async (span) => { + try { + let funcResponse + const isLMI = (this.data.LambdaFunctionNode?.configuration as any)?.CapacityProviderConfig + if (remoteDebugEnabled) { + funcResponse = await this.clientDebug.invoke(this.data.FunctionArn, input, qualifier) + } else if (isLMI) { + funcResponse = await this.client.invoke(this.data.FunctionArn, input, qualifier, 'None') + } else { + funcResponse = await this.client.invoke(this.data.FunctionArn, input, qualifier, 'Tail') + } - try { - const funcResponse = await this.client.invoke(this.data.FunctionArn, input, qualifier) - const logs = funcResponse.LogResult ? decodeBase64(funcResponse.LogResult) : '' - const payload = funcResponse.Payload ? funcResponse.Payload : JSON.stringify({}) - - this.channel.appendLine(`Invocation result for ${this.data.FunctionArn}`) - this.channel.appendLine('Logs:') - this.channel.appendLine(logs) - this.channel.appendLine('') - this.channel.appendLine('Payload:') - this.channel.appendLine(String(payload)) - this.channel.appendLine('') - } catch (e) { - const error = e as Error - this.channel.appendLine(`There was an error invoking ${this.data.FunctionArn}`) - this.channel.appendLine(error.toString()) - this.channel.appendLine('') - result = 'Failed' - } finally { - telemetry.lambda_invokeRemote.emit({ - result, - passive: false, - source: source, - runtimeString: this.data.Runtime, - action: remoteDebugEnabled ? 'debug' : 'invoke', - }) + const logs = funcResponse.LogResult ? decodeBase64(funcResponse.LogResult) : '' + const decodedPayload = funcResponse.Payload ? new TextDecoder().decode(funcResponse.Payload) : '' + const payload = decodedPayload || JSON.stringify({}) - // Update the session state to indicate we've finished invoking - this.isInvoking = false + this.channel.appendLine(`Invocation result for ${this.data.FunctionArn}`) + if (!isLMI) { + this.channel.appendLine('Logs:') + this.channel.appendLine(logs) + this.channel.appendLine('') + } + this.channel.appendLine('Payload:') + this.channel.appendLine(String(payload)) + this.channel.appendLine('') + } catch (e) { + const error = e as Error + this.channel.appendLine(`There was an error invoking ${this.data.FunctionArn}`) + this.channel.appendLine(error.toString()) + this.channel.appendLine('') + } finally { + let action = remoteDebugEnabled ? 'debug' : 'invoke' + if (!this.data.isLambdaRemote) { + action = `${action}LocalStack` + } + span.record({ + passive: false, + source: source, + runtimeString: this.data.Runtime, + action: action, + }) + + // Update the session state to indicate we've finished invoking + this.isInvoking = false - // If debugging is active, restart the timer - if (RemoteDebugController.instance.isDebugging) { - this.startDebugTimer() + // If debugging is active, restart the timer + if (RemoteDebugController.instance.isDebugging) { + this.startDebugTimer() + } + this.channel.show() } - this.channel.show() - } + }) } public async promptFile() { @@ -367,13 +380,17 @@ export class RemoteInvokeWebview extends VueWebview { this.data.LambdaFunctionNode?.configuration.Handler ) getLogger().warn(warning) - void vscode.window.showWarningMessage(warning) + void showMessage('warn', warning) } return fileLocations[0].fsPath } public async tryOpenHandlerFile(path?: string, watchForUpdates: boolean = true): Promise { this.handlerFile = undefined + if (this.data.LocalRootPath) { + // don't watch in appbuilder + watchForUpdates = false + } if (path) { // path is provided, override init path this.data.LocalRootPath = path @@ -383,18 +400,20 @@ export class RemoteInvokeWebview extends VueWebview { return false } - const handlerFile = await getLambdaHandlerFile( - vscode.Uri.file(this.data.LocalRootPath), - '', - this.data.LambdaFunctionNode?.configuration.Handler ?? '', - this.data.Runtime ?? 'unknown' - ) + const handlerFile = this.data.Runtime + ? await getLambdaHandlerFile( + vscode.Uri.file(this.data.LocalRootPath), + '', + this.data.LambdaFunctionNode?.configuration.Handler ?? '', + this.data.Runtime + ) + : undefined if (!handlerFile || !(await fs.exists(handlerFile))) { this.handlerFileAvailable = false return false } this.handlerFileAvailable = true - if (watchForUpdates) { + if (watchForUpdates && !isHotReloadingFunction(this.data.LambdaFunctionNode?.configuration.CodeSha256)) { this.setupFileWatcher() } await openLambdaFile(handlerFile.fsPath) @@ -433,22 +452,166 @@ export class RemoteInvokeWebview extends VueWebview { } public async listRemoteTestEvents(functionArn: string, region: string): Promise { - const params: SamCliRemoteTestEventsParameters = { - functionArn: functionArn, - operation: TestEventsOperation.List, - region: region, + try { + const params: SamCliRemoteTestEventsParameters = { + functionArn: functionArn, + operation: TestEventsOperation.List, + region: region, + } + const result = await this.remoteTestEvents(params) + return result.split('\n').filter((event) => event.trim() !== '') + } catch (error) { + // Suppress "lambda-testevent-schemas registry not found" error - this is normal when no test events exist + const errorMessage = error instanceof Error ? error.message : String(error) + if ( + errorMessage.includes('lambda-testevent-schemas registry not found') || + errorMessage.includes('There are no saved events') + ) { + getLogger().debug('No remote test events found for function: %s', functionArn) + return [] + } + // Re-throw other errors + throw error } - const result = await this.remoteTestEvents(params) - return result.split('\n') } - public async createRemoteTestEvents(putEvent: Event) { + public async selectRemoteTestEvent(functionArn: string, region: string): Promise { + let events: string[] = [] + + try { + events = await this.listRemoteTestEvents(functionArn, region) + } catch (error) { + getLogger().error('Failed to list remote test events: %O', error) + void showMessage( + 'error', + localize('AWS.lambda.remoteInvoke.failedToListEvents', 'Failed to list remote test events') + ) + return undefined + } + + if (events.length === 0) { + void showMessage( + 'info', + localize( + 'AWS.lambda.remoteInvoke.noRemoteEvents', + 'No remote test events found. You can create one using "Save as remote event".' + ) + ) + return undefined + } + + const selected = await vscode.window.showQuickPick(events, { + placeHolder: localize('AWS.lambda.remoteInvoke.selectRemoteEvent', 'Select a remote test event'), + title: localize('AWS.lambda.remoteInvoke.loadRemoteEvent', 'Load Remote Test Event'), + }) + + if (selected) { + const eventData = { + name: selected, + region: region, + arn: functionArn, + } + const resp = await this.getRemoteTestEvents(eventData) + return resp + } + + return undefined + } + + public async saveRemoteTestEvent( + functionArn: string, + region: string, + eventContent: string + ): Promise { + let events: string[] = [] + + try { + events = await this.listRemoteTestEvents(functionArn, region) + } catch (error) { + // Log error but continue - user can still create new events + getLogger().debug('Failed to list existing remote test events (may not exist yet): %O', error) + } + + // Create options for quickpick + const createNewOption = '$(add) Create new test event' + const options = events.length > 0 ? [createNewOption, ...events] : [createNewOption] + + const selected = await vscode.window.showQuickPick(options, { + placeHolder: localize( + 'AWS.lambda.remoteInvoke.saveEventChoice', + 'Create new or overwrite existing test event' + ), + title: localize('AWS.lambda.remoteInvoke.saveRemoteEvent', 'Save as Remote Event'), + }) + + if (!selected) { + return undefined + } + + let eventName: string | undefined + + if (selected === createNewOption) { + // Prompt for new event name + eventName = await vscode.window.showInputBox({ + prompt: localize('AWS.lambda.remoteInvoke.enterEventName', 'Enter a name for the test event'), + placeHolder: localize('AWS.lambda.remoteInvoke.eventNamePlaceholder', 'MyTestEvent'), + validateInput: (value) => { + if (!value || value.trim() === '') { + return localize('AWS.lambda.remoteInvoke.eventNameRequired', 'Event name is required') + } + if (events.includes(value)) { + return localize( + 'AWS.lambda.remoteInvoke.eventNameExists', + 'An event with this name already exists' + ) + } + return undefined + }, + }) + } else { + // Use selected existing event name + const confirm = await showConfirmationMessage({ + prompt: localize( + 'AWS.lambda.remoteInvoke.overwriteEvent', + 'Overwrite existing test event "{0}"?', + selected + ), + confirm: localize('AWS.lambda.remoteInvoke.overwrite', 'Overwrite'), + cancel: 'Cancel', + type: 'warning', + }) + + if (confirm) { + eventName = selected + } + } + + if (eventName) { + // Use force flag when overwriting existing events + const isOverwriting = selected !== createNewOption + const params: SamCliRemoteTestEventsParameters = { + functionArn: functionArn, + operation: TestEventsOperation.Put, + name: eventName, + eventSample: eventContent, + region: region, + force: isOverwriting, + } + await this.remoteTestEvents(params) + return eventName + } + + return undefined + } + + public async createRemoteTestEvents(putEvent: Event, force: boolean = false) { const params: SamCliRemoteTestEventsParameters = { functionArn: putEvent.arn, operation: TestEventsOperation.Put, name: putEvent.name, eventSample: putEvent.event, region: putEvent.region, + force: force, } return await this.remoteTestEvents(params) } @@ -507,7 +670,7 @@ export class RemoteInvokeWebview extends VueWebview { // Download lambda code and update the local root path public async downloadRemoteCode(): Promise { return await telemetry.lambda_import.run(async (span) => { - span.record({ runtime: this.data.Runtime as Runtime | undefined, source: 'RemoteDebug' }) + span.record({ runtime: this.data.Runtime as TelemetryRuntime | undefined, source: 'RemoteDebug' }) try { if (this.data.LambdaFunctionNode) { const output = await runDownloadLambda(this.data.LambdaFunctionNode, true) @@ -539,7 +702,8 @@ export class RemoteInvokeWebview extends VueWebview { // this serves as a lock for invoke public checkReadyToInvoke(): boolean { if (this.isInvoking) { - void vscode.window.showWarningMessage( + void showMessage( + 'warn', localize( 'AWS.lambda.remoteInvoke.invokeInProgress', 'A remote invoke is already in progress, please wait for previous invoke, or remove debug setup' @@ -548,12 +712,14 @@ export class RemoteInvokeWebview extends VueWebview { return false } if (this.isStartingDebug) { - void vscode.window.showWarningMessage( + void showMessage( + 'warn', localize( 'AWS.lambda.remoteInvoke.debugSetupInProgress', 'A debugger setup is already in progress, please wait for previous setup to complete, or remove debug setup' ) ) + return false } return true } @@ -617,6 +783,8 @@ export class RemoteInvokeWebview extends VueWebview { await RemoteDebugController.instance.startDebugging(this.data.FunctionArn, this.data.Runtime ?? 'unknown', { ...config, handlerFile: this.handlerFile, + samFunctionLogicalId: this.data.LambdaFunctionNode.logicalId, + samProjectRoot: this.data.LambdaFunctionNode.projectRoot, }) } catch (e) { throw ToolkitError.chain( @@ -668,7 +836,10 @@ export class RemoteInvokeWebview extends VueWebview { // prestatus check run at checkbox click public async debugPreCheck(): Promise { return await telemetry.lambda_remoteDebugPrecheck.run(async (span) => { - span.record({ runtimeString: this.data.Runtime, source: 'webview' }) + span.record({ + runtimeString: this.data.Runtime, + source: this.data.isLambdaRemote ? 'webview' : 'webviewLocalStack', + }) if (!this.debugging && RemoteDebugController.instance.isDebugging) { // another debug session in progress const result = await showConfirmationMessage({ @@ -744,20 +915,21 @@ export async function invokeRemoteLambda( const resource: LambdaFunctionNode = params.functionNode const source: string = params.source || 'AwsExplorerRemoteInvoke' const client = getLambdaClientWithAgent(resource.regionCode) + const clientDebug = getLambdaClientWithAgent(resource.regionCode, getLambdaDebugUserAgentPairs()) const Panel = VueWebview.compilePanel(RemoteInvokeWebview) // Initialize support and debugging capabilities - const runtime = resource.configuration.Runtime ?? 'unknown' + const runtime = resource.configuration.Runtime const region = resource.regionCode - const supportCodeDownload = RemoteDebugController.instance.supportCodeDownload(runtime) - const runtimeSupportsRemoteDebug = RemoteDebugController.instance.supportRuntimeRemoteDebug(runtime) - const remoteDebugLayer = RemoteDebugController.instance.getRemoteDebugLayer( - region, - resource.configuration.Architectures + const supportCodeDownload = RemoteDebugController.instance.supportCodeDownload( + runtime, + resource.configuration.CodeSha256 ) + const runtimeSupportsRemoteDebug = RemoteDebugController.instance.supportRuntimeRemoteDebug(runtime) + const remoteDebugLayer = getRemoteDebugLayer(region, resource.configuration.Architectures) - const wv = new Panel(context.extensionContext, context.outputChannel, client, { + const wv = new Panel(context.extensionContext, context.outputChannel, client, clientDebug, { FunctionName: resource.configuration.FunctionName ?? '', FunctionArn: resource.configuration.FunctionArn ?? '', FunctionRegion: resource.regionCode, @@ -770,6 +942,7 @@ export async function invokeRemoteLambda( supportCodeDownload: supportCodeDownload, runtimeSupportsRemoteDebug: runtimeSupportsRemoteDebug, remoteDebugLayer: remoteDebugLayer, + isLambdaRemote: !isLocalStackConnection(), }) // focus on first group so wv will show up in the side await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup') diff --git a/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.css b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.css index bb7d5054bf2..c96291b26ae 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.css +++ b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.css @@ -1,3 +1,4 @@ +/* Container and Layout */ .Icontainer { margin-inline: auto; margin-top: 2rem; @@ -15,88 +16,101 @@ div { width: 100%; } -.form-row { - display: grid; - grid-template-columns: 150px 1fr; +/* VSCode Settings Style Layout */ +.vscode-setting-item { margin-bottom: 10px; + padding: 5px 0; +} + +.setting-header { + display: flex; align-items: center; + margin-bottom: 8px; } -.form-row-no-align { - display: grid; - grid-template-columns: 150px 1fr; - margin-bottom: 10px; +.setting-title { + font-weight: 600; + font-size: 14px; + margin: 0; } -.form-double-row { - display: grid; - grid-template-rows: 20px 1fr; - margin-inline: 0px; - padding: 0px 0px; - align-items: center; +.setting-body { + display: flex; + align-items: flex-start; + gap: 8px; } -.form-row-select { - width: 100%; - max-width: 387px; - height: 28px; - border: 1px; - border-radius: 5px; - gap: 4px; - padding: 2px 8px; -} - -.dynamic-span { - white-space: nowrap; - text-overflow: initial; - overflow: auto; - width: 100%; - max-width: 381px; - height: auto; - font-weight: 500; - font-size: 13px; - line-height: 15.51px; +.setting-description { + flex: 1; } -.form-row-event-select { - width: 100%; - max-width: 244px; - height: 28px; - margin-bottom: 15px; - margin-left: 8px; +.setting-description info-wrap, +.setting-description info { + display: block; + margin-bottom: 4px; } -.payload-options { +.setting-description-full { + margin-bottom: 8px; +} + +.setting-description-full info-wrap { + display: block; + margin-bottom: 4px; +} + +.setting-input-group-full { + display: flex; + align-items: center; + gap: 5px; +} + +.setting-input { + flex-grow: 1; + margin-right: 2px; +} + +/* Form Layout Classes - Base grid layout shared by multiple classes */ +.form-row, +.form-row-no-align { display: grid; grid-template-columns: 150px 1fr; - align-items: center; margin-bottom: 10px; } +.form-row { + align-items: center; +} + +.form-double-row { + display: grid; + grid-template-rows: 20px 1fr; + align-items: center; +} + +/* Typography and Text Elements */ label { font-weight: 500; font-size: 14px; margin-right: 10px; } -info { +/* Merge info and info-wrap as they share most properties */ +info, +info-wrap { color: var(--vscode-descriptionForeground); font-weight: 500; font-size: 13px; margin-right: 10px; - text-wrap-mode: nowrap; } -info-wrap { - color: var(--vscode-descriptionForeground); - font-weight: 500; - font-size: 13px; - margin-right: 10px; +info { + text-wrap-mode: nowrap; } +/* Form Elements */ span, -select, -.payload-options { +select { display: block; } @@ -109,121 +123,79 @@ textarea { resize: none; } -.payload-options-button { - display: grid; - align-items: center; - border: none; - padding: 5px 10px; - cursor: pointer; - font-size: 0.9em; - margin-bottom: 10px; +/* Button Styles */ +.button-theme-primary, +.button-theme-inline { + border: 1px solid var(--vscode-button-border); } .button-theme-primary { + padding: 8px 12px; color: var(--vscode-button-foreground); background: var(--vscode-button-background); - border: 1px solid var(--vscode-button-border); - padding: 8px 12px; } + .button-theme-primary:hover:not(:disabled) { background: var(--vscode-button-hoverBackground); cursor: pointer; } -.button-theme-secondary { - color: var(--vscode-button-secondaryForeground); - background: var(--vscode-button-secondaryBackground); - border: 1px solid var(--vscode-button-border); - padding: 8px 12px; -} -.button-theme-secondary:hover:not(:disabled) { - background: var(--vscode-button-secondaryHoverBackground); - cursor: pointer; -} .button-theme-inline { + padding: 4px 6px; color: var(--vscode-button-secondaryForeground); background: var(--vscode-button-secondaryBackground); - border: 1px solid var(--vscode-button-border); - padding: 4px 6px; } + .button-theme-inline:hover:not(:disabled) { background: var(--vscode-button-secondaryHoverBackground); cursor: pointer; } -.payload-options-buttons { - display: flex; - align-items: center; - margin-top: 10px; - margin-bottom: 10px; -} - -.radio-selector { - width: 15px; - height: 15px; - border-radius: 50%; -} - -.label-selector { - padding-left: 7px; - font-weight: 500; - font-size: 13px; - line-height: 15.51px; - text-align: center; -} - -.form-row-select { - display: grid; - grid-template-columns: 150px 1fr; - margin-bottom: 10px; +button:disabled { + opacity: 0.5; + cursor: not-allowed; } -.formfield { +/* Payload Section Styles */ +.payload-button-group { display: flex; - align-items: center; - margin-bottom: 0.5rem; + gap: 5px; + margin-bottom: 10px; } -.debug-timer { - padding: 5px 10px; - background-color: var(--vscode-editorWidget-background); - border-radius: 4px; - font-weight: 500; +.payload-textarea { + width: 100%; + min-height: 200px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + line-height: 1.5; } +/* Collapsible Section */ .collapsible-section { margin: 15px 0; border: 1px solid var(--vscode-widget-border); border-radius: 4px; } +.collapsible-header, +.collapsible-content { + max-width: 96%; +} + .collapsible-header { padding: 8px 12px; background-color: var(--vscode-sideBarSectionHeader-background); cursor: pointer; font-weight: 500; - max-width: 96%; } .collapsible-content { padding: 10px; border-top: 1px solid var(--vscode-widget-border); - max-width: 96%; -} - -/* Ensure buttons in the same line are properly spaced */ -.button-container { - display: flex; - gap: 5px; } -/* For buttons that should be disabled */ -button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Validation error styles */ +/* Validation and Error Styles */ .input-error { border: 1px solid var(--vscode-inputValidation-errorBorder) !important; background-color: var(--vscode-inputValidation-errorBackground) !important; @@ -237,7 +209,7 @@ button:disabled { line-height: 1.2; } -/* Enhanced styling for remote debug checkbox to make it more obvious in dark mode */ +/* Checkbox and Status Styles */ .remote-debug-checkbox { width: 18px !important; height: 18px !important; @@ -245,7 +217,6 @@ button:disabled { border: 2px solid var(--vscode-checkbox-border) !important; border-radius: 3px !important; background-color: var(--vscode-checkbox-background) !important; - border-color: var(--vscode-checkbox-selectBorder) !important; cursor: pointer; } @@ -257,8 +228,8 @@ button:disabled { .remote-debug-checkbox:disabled { opacity: 0.6; cursor: not-allowed; - border-color: var(--vscode-checkbox-border); - background-color: var(--vscode-input-background); + border-color: var(--vscode-checkbox-border) !important; + background-color: var(--vscode-input-background) !important; } .remote-debug-checkbox:focus { diff --git a/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue index 1743fd4ef00..36c1a997d5a 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue +++ b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue @@ -21,7 +21,10 @@
- {{ initialData.FunctionRegion }} + {{ initialData.FunctionRegion }} + {{ uiState.extraRegionInfo }}
@@ -30,107 +33,126 @@
-
-
-
+
+
+ + + Auto remove after 60 second of inactive time +
+
- - - - Auto remove after 60 second of inactive time - - Runtime {{ initialData.Runtime }} and region {{ initialData.FunctionRegion }} don't support - remote debugging yet - - - Runtime {{ initialData.Runtime }} doesn't support remote debugging - - - Region {{ initialData.FunctionRegion }} doesn't support remote debugging yet - -
-
- -
- - Remote debugging is not recommended for production environments. The AWS Toolkit modifies your - function by deploying it with an additional layer to enable remote debugging. Your local code - breakpoints are then used to step through the remote function invocation. - Learn more - -
- -
- -
-
+
Your handler file has been located. You can now set breakpoints in this file for debugging. - (open handler) + v-if=" + initialData.runtimeSupportsRemoteDebug && + initialData.remoteDebugLayer && + initialData.LambdaFunctionNode?.configuration.SnapStart + " + > + Remote debugging is not recommended for production environments. The AWS Toolkit modifies + your function by deploying it with an additional layer to enable remote debugging. Your + local code breakpoints are then used to step through the remote function invocation. + Learn more -
-
- Specify the path to your local directory that contains the handler file for debugging, or - download the handler file from your deployed function. -
-
- Specify the path to your local directory that contains the handler file for - debugging. + -
-
- - - + Lambda Managed Instances Function doesn't support remote debugging yet +
+
+
+ +
+
+ + Your handler file has been located. You can now open handler to set breakpoints in this + file for debugging. + + + Browse to specify the absolute path to your local directory that contains the handler + file for debugging. Or Download the handler file from your deployed function. + + + Browse to specify the absolute path to your local directory that contains the handler + file for debugging. + +
+
+ + + + +
+
+
@@ -147,7 +169,7 @@
@@ -162,15 +184,24 @@
{{ debugPortError }}
-
+
-
+
{{ lambdaTimeoutError }}
-
+
{{ lambdaLayerError }}
@@ -244,7 +275,7 @@
@@ -280,128 +311,44 @@ type="text" v-model="runtimeSettings.projectName" placeholder="YourJavaProjectName" - title="The name of the Java project for debuging" + title="The name of the Java project for debugging" />
- -
-
-
-
-
-
-
- -
-
-
- -
-
-
- - -
-
-
-
-
-
- -
-
- -
-
-
-
- + +
+
+
-
-
-
- - -   {{ payloadData.selectedFile || 'No file selected' }} -
+
+ + Enter the JSON payload for your Lambda function invocation. You can Load sample event from + AWS event templates, Load local file from your computer + + Load remote event from your saved test events. You can Save as remote event to save + the event below for future use
-
-
-
-
- -
-
-   - -
-
-
- - -
-
-
- - -
- +
+ + + +
+
diff --git a/packages/core/src/lambda/vue/remoteInvoke/remoteInvokeFrontend.ts b/packages/core/src/lambda/vue/remoteInvoke/remoteInvokeFrontend.ts index b2253a46fd2..a99b6ac075e 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/remoteInvokeFrontend.ts +++ b/packages/core/src/lambda/vue/remoteInvoke/remoteInvokeFrontend.ts @@ -9,7 +9,7 @@ import { defineComponent } from 'vue' import { WebviewClientFactory } from '../../../webviews/client' import saveData from '../../../webviews/mixins/saveData' -import { RemoteInvokeData, RemoteInvokeWebview } from './invokeLambda' +import type { RemoteInvokeData, RemoteInvokeWebview } from './invokeLambda' const client = WebviewClientFactory.create() const defaultInitialData = { @@ -25,6 +25,7 @@ const defaultInitialData = { supportCodeDownload: true, runtimeSupportsRemoteDebug: true, remoteDebugLayer: '', + isLambdaRemote: true, } export default defineComponent({ @@ -32,7 +33,7 @@ export default defineComponent({ return { initialData: { ...defaultInitialData }, debugConfig: { - debugPort: 9229, + debugPort: undefined, localRootPath: '', remoteRootPath: '/var/task', shouldPublishVersion: true, @@ -56,16 +57,10 @@ export default defineComponent({ }, uiState: { isCollapsed: true, - showNameInput: false, - payload: 'sampleEvents', + extraRegionInfo: '', }, payloadData: { - selectedSampleRequest: '', sampleText: '{}', - selectedFile: '', - selectedFilePath: '', - selectedTestEvent: '', - newTestEventName: '', }, } }, @@ -173,6 +168,9 @@ export default defineComponent({ // Sync state from workspace storage async syncStateFromWorkspace() { try { + // Detect Lambda remote debugging connection + this.uiState.extraRegionInfo = this.initialData.isLambdaRemote ? '' : '(LocalStack running)' + // Update debugging state this.debugState.isDebugging = await client.isWebViewDebugging() this.debugConfig.localRootPath = await client.getLocalPath() @@ -197,36 +195,21 @@ export default defineComponent({ console.error('Failed to sync state from workspace:', error) } }, - async newSelection() { - const eventData = { - name: this.payloadData.selectedTestEvent, - region: this.initialData.FunctionRegion, - arn: this.initialData.FunctionArn, - } - const resp = await client.getRemoteTestEvents(eventData) - this.payloadData.sampleText = JSON.stringify(JSON.parse(resp), undefined, 4) - }, async saveEvent() { - const eventData = { - name: this.payloadData.newTestEventName, - event: this.payloadData.sampleText, - region: this.initialData.FunctionRegion, - arn: this.initialData.FunctionArn, + if (this.initialData.FunctionArn && this.initialData.FunctionRegion) { + // Use the backend method that shows a quickpick for save + await client.saveRemoteTestEvent( + this.initialData.FunctionArn, + this.initialData.FunctionRegion, + this.payloadData.sampleText + ) } - await client.createRemoteTestEvents(eventData) - this.uiState.showNameInput = false - this.payloadData.newTestEventName = '' - this.payloadData.selectedTestEvent = eventData.name - this.initialData.TestEvents = await client.listRemoteTestEvents( - this.initialData.FunctionArn, - this.initialData.FunctionRegion - ) }, async promptForFileLocation() { const resp = await client.promptFile() if (resp) { - this.payloadData.selectedFile = resp.selectedFile - this.payloadData.selectedFilePath = resp.selectedFilePath + // Populate the textarea with file content + this.payloadData.sampleText = resp.sample } }, async promptForFolderLocation() { @@ -236,23 +219,6 @@ export default defineComponent({ this.debugState.handlerFileAvailable = await client.getHandlerAvailable() } }, - - onFileChange(event: Event) { - const input = event.target as HTMLInputElement - if (input.files && input.files.length > 0) { - const file = input.files[0] - this.payloadData.selectedFile = file.name - - // Use Blob.text() to read the file as text - file.text() - .then((text) => { - this.payloadData.sampleText = text - }) - .catch((error) => { - console.error('Error reading file:', error) - }) - } - }, async debugPreCheck() { if (!this.debugState.remoteDebuggingEnabled) { // don't check if unchecking @@ -267,11 +233,6 @@ export default defineComponent({ this.debugState.handlerFileAvailable = await client.getHandlerAvailable() } }, - showNameField() { - if (this.initialData.FunctionRegion || this.initialData.FunctionRegion) { - this.uiState.showNameInput = true - } - }, async sendInput() { // Tell the backend to set the button state. This state is maintained even if webview loses focus @@ -295,11 +256,13 @@ export default defineComponent({ return } + const defaultPort = this.initialData.isLambdaRemote ? 9229 : undefined + if (!this.debugState.isDebugging) { this.debugState.isDebugging = await client.startDebugging({ functionArn: this.initialData.FunctionArn, functionName: this.initialData.FunctionName, - port: this.debugConfig.debugPort ?? 9229, + port: this.debugConfig.debugPort ?? defaultPort, sourceMap: this.runtimeSettings.sourceMapEnabled, localRoot: this.debugConfig.localRootPath, shouldPublishVersion: this.debugConfig.shouldPublishVersion, @@ -315,6 +278,7 @@ export default defineComponent({ layerArn: this.initialData.remoteDebugLayer, lambdaTimeout: this.debugConfig.lambdaTimeout ?? 900, outFiles: this.runtimeSettings.outFiles?.split(','), + isLambdaRemote: this.initialData.isLambdaRemote ?? true, }) if (!this.debugState.isDebugging) { // user cancel or failed to start debugging @@ -324,20 +288,11 @@ export default defineComponent({ this.debugState.showDebugTimer = false } - let event = '' - - if (this.uiState.payload === 'sampleEvents' || this.uiState.payload === 'savedEvents') { - event = this.payloadData.sampleText - } else if (this.uiState.payload === 'localFile') { - if (this.payloadData.selectedFile && this.payloadData.selectedFilePath) { - const resp = await client.loadFile(this.payloadData.selectedFilePath) - if (resp) { - event = resp.sample - } - } - } - - await client.invokeLambda(event, this.initialData.Source, this.debugState.remoteDebuggingEnabled) + await client.invokeLambda( + this.payloadData.sampleText, + this.initialData.Source, + this.debugState.remoteDebuggingEnabled + ) await this.syncStateFromWorkspace() }, @@ -415,16 +370,25 @@ export default defineComponent({ }, async loadRemoteTestEvents() { - const shouldLoadEvents = - this.uiState.payload === 'savedEvents' && - this.initialData.FunctionArn && - this.initialData.FunctionRegion - - if (shouldLoadEvents) { - this.initialData.TestEvents = await client.listRemoteTestEvents( + if (this.initialData.FunctionArn && this.initialData.FunctionRegion) { + // Use the backend method that shows a quickpick + const eventContent = await client.selectRemoteTestEvent( this.initialData.FunctionArn, this.initialData.FunctionRegion ) + + if (eventContent) { + // Populate the textarea with the selected event + this.payloadData.sampleText = JSON.stringify(JSON.parse(eventContent), undefined, 4) + } + } + }, + onDebugPortChange(event: Event) { + const value = (event.target as HTMLInputElement).value + if (value === '') { + this.debugConfig.debugPort = undefined + } else { + this.debugConfig.debugPort = Number(value) } }, }, diff --git a/packages/core/src/lambda/wizards/samInitWizard.ts b/packages/core/src/lambda/wizards/samInitWizard.ts index 10906ec513d..9bd3a1b72fb 100644 --- a/packages/core/src/lambda/wizards/samInitWizard.ts +++ b/packages/core/src/lambda/wizards/samInitWizard.ts @@ -5,7 +5,7 @@ import * as nls from 'vscode-nls' import * as AWS from '@aws-sdk/types' -import { Runtime } from 'aws-sdk/clients/lambda' +import { Runtime } from '@aws-sdk/client-lambda' import * as path from 'path' import * as vscode from 'vscode' import { SchemasDataProvider } from '../../eventSchemas/providers/schemasDataProvider' @@ -231,7 +231,7 @@ export class CreateNewSamAppWizard extends Wizard { return false } - return samArmLambdaRuntimes.has(state.runtimeAndPackage?.runtime ?? 'unknown') + return state.runtimeAndPackage ? samArmLambdaRuntimes.has(state.runtimeAndPackage.runtime) : false } this.form.architecture.bindPrompter(createArchitecturePrompter, { diff --git a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts index 4e4db35b9ad..f6a80d8c3c2 100644 --- a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts +++ b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts @@ -28,6 +28,7 @@ import globals from '../../../../shared/extensionGlobals' export class ToolkitLoginWebview extends CommonAuthWebview { public override id: string = 'aws.toolkit.AmazonCommonAuth' public static sourcePath: string = 'vue/src/login/webview/vue/toolkit/index.js' + public override supportsLoadTelemetry: boolean = true private isCodeCatalystLogin = false override onActiveConnectionModified: vscode.EventEmitter = new vscode.EventEmitter() diff --git a/packages/core/src/sagemakerunifiedstudio/activation.ts b/packages/core/src/sagemakerunifiedstudio/activation.ts new file mode 100644 index 00000000000..9c47137d6da --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/activation.ts @@ -0,0 +1,27 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { activate as activateConnectionMagicsSelector } from './connectionMagicsSelector/activation' +import { activate as activateExplorer } from './explorer/activation' +import { isSageMaker } from '../shared/extensionUtilities' +import { initializeResourceMetadata } from './shared/utils/resourceMetadataUtils' +import { setContext } from '../shared/vscode/setContext' +import { SmusUtils } from './shared/smusUtils' +import * as smusUriHandlers from './uriHandlers' +import { ExtContext } from '../shared/extensions' + +export async function activate(ctx: ExtContext): Promise { + // Only run when environment is a SageMaker Unified Studio space + if (isSageMaker('SMUS') || isSageMaker('SMUS-SPACE-REMOTE-ACCESS')) { + await initializeResourceMetadata() + // Setting context before any getContext calls to avoid potential race conditions. + await setContext('aws.smus.inSmusSpaceEnvironment', SmusUtils.isInSmusSpaceEnvironment()) + await activateConnectionMagicsSelector(ctx.extensionContext) + } + await activateExplorer(ctx.extensionContext) + + // Register SMUS URI handler for deeplink connections + ctx.extensionContext.subscriptions.push(smusUriHandlers.register(ctx)) +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/authenticationOrchestrator.ts b/packages/core/src/sagemakerunifiedstudio/auth/authenticationOrchestrator.ts new file mode 100644 index 00000000000..208f7ccd95c --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/authenticationOrchestrator.ts @@ -0,0 +1,330 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../shared/logger/logger' +import { ToolkitError } from '../../shared/errors' +import { SmusErrorCodes } from '../shared/smusUtils' +import { SmusAuthenticationProvider } from './providers/smusAuthenticationProvider' + +import { SmusSsoAuthenticationUI } from './ui/ssoAuthentication' +import { + SmusIamProfileSelector, + IamProfileSelection, + IamProfileEditingInProgress, + IamProfileBackNavigation, +} from './ui/iamProfileSelection' +import { SmusAuthenticationPreferencesManager } from './preferences/authenticationPreferences' +import { DataZoneCustomClientHelper } from '../shared/client/datazoneCustomClientHelper' +import { recordAuthTelemetry } from '../shared/telemetry' + +export type SmusAuthenticationMethod = 'sso' | 'iam' + +export type SmusAuthenticationResult = + | { status: 'SUCCESS' } + | { status: 'BACK' } + | { status: 'EDITING' } + | { status: 'INVALID_PROFILE'; error: string } + +/** + * Orchestrates SMUS authentication flows + */ +export class SmusAuthenticationOrchestrator { + private static readonly logger = getLogger('smus') + + /** + * Handles IAM authentication flow + * @param authProvider The SMUS authentication provider + * @param span Telemetry span + * @param context Extension context + * @param existingProfileName Optional profile name to re-authenticate with (skips profile selection) + * @param existingRegion Optional region to use (skips region selection) + */ + public static async handleIamAuthentication( + authProvider: SmusAuthenticationProvider, + span: any, + context: vscode.ExtensionContext, + existingProfileName?: string, + existingRegion?: string + ): Promise { + const logger = this.logger + + try { + let profileSelection: IamProfileSelection | IamProfileEditingInProgress | IamProfileBackNavigation + + // If profile and region are provided, skip profile selection (re-authentication case) + if (existingProfileName && existingRegion) { + logger.debug( + `Auth: Re-authenticating with existing profile: ${existingProfileName}, region: ${existingRegion}` + ) + profileSelection = { + profileName: existingProfileName, + region: existingRegion, + } + } else { + // Show IAM profile selection dialog + profileSelection = await SmusIamProfileSelector.showIamProfileSelection() + } + + // Handle different result types + if ('isBack' in profileSelection) { + // User chose to go back to authentication method selection + logger.debug('User chose to go back to authentication method selection') + return { status: 'BACK' } + } + + if ('isEditing' in profileSelection) { + // User chose to edit credentials or is in editing mode + logger.debug('User is editing credentials') + return { status: 'EDITING' } + } + + // At this point, we have a profile selected + logger.debug(`Selected profile: ${profileSelection.profileName}, region: ${profileSelection.region}`) + + // Validate the selected profile + const validation = await authProvider.validateIamProfile(profileSelection.profileName) + if (!validation.isValid) { + logger.debug(`Profile validation failed: ${validation.error}`) + return { status: 'INVALID_PROFILE', error: validation.error || 'Profile validation failed' } + } + + // Discover IAM-based domain using IAM credential. If IAM-based domain is not present, we should throw an appropriate error + // and exit + logger.debug('Discovering IAM-based domain using IAM credentials') + + const domainUrl = await this.findSmusIamDomain( + authProvider, + profileSelection.profileName, + profileSelection.region + ) + if (!domainUrl) { + throw new ToolkitError('No IAM-based domains found in the specified region', { + code: SmusErrorCodes.IamDomainNotFound, + cancelled: true, + }) + } + + // Connect using IAM profile with IAM-based domain flag + const connection = await authProvider.connectWithIamProfile( + profileSelection.profileName, + profileSelection.region, + domainUrl, + true // isIamDomain - we found an IAM-based domain + ) + + if (!connection) { + throw new ToolkitError('Failed to establish IAM connection', { + code: SmusErrorCodes.FailedAuthConnecton, + }) + } + + logger.info( + `Successfully connected with IAM profile ${profileSelection.profileName} in region ${profileSelection.region} to IAM-based domain` + ) + + // Extract domain ID and region for telemetry logging + const domainId = connection.domainId + const region = authProvider.getDomainRegion() + + logger.info(`Connected to SageMaker Unified Studio domain: ${domainId} in region ${region}`) + await this.recordAuthTelemetry(span, authProvider, domainId, region) + + // Refresh the tree view to show authenticated state + try { + await vscode.commands.executeCommand('aws.smus.rootView.refresh') + } catch (refreshErr) { + logger.debug(`Failed to refresh views after login: ${(refreshErr as Error).message}`) + } + + // After successful IAM authentication (IAM mode), automatically open project picker + logger.debug('IAM authentication successful, opening project picker') + try { + await vscode.commands.executeCommand('aws.smus.switchProject') + } catch (pickerErr) { + logger.debug(`Failed to open project picker: ${(pickerErr as Error).message}`) + } + + // Ask to remember authentication method preference (non-blocking) + void this.askToRememberAuthMethod(context, 'iam') + + // Return success to complete the authentication flow gracefully + return { status: 'SUCCESS' } + } catch (error) { + // Handle user cancellation (including editing mode) + if ( + error instanceof ToolkitError && + (error.code === SmusErrorCodes.UserCancelled || error.code === SmusErrorCodes.IamDomainNotFound) + ) { + logger.debug('IAM authentication cancelled by user or failed due to customer error') + throw error // Re-throw to be handled by the main loop + } else { + // Log the error for actual failures + logger.error('IAM authentication failed: %s', (error as Error).message) + throw error + } + } + } + + /** + * Handles SSO authentication flow + */ + public static async handleSsoAuthentication( + authProvider: SmusAuthenticationProvider, + span: any, + context: vscode.ExtensionContext + ): Promise { + const logger = this.logger + logger.debug('Starting SSO authentication flow') + + // Show domain URL input dialog with back button support + const domainUrl = await SmusSsoAuthenticationUI.showDomainUrlInput() + + logger.debug(`Domain URL input result: ${domainUrl ? 'provided' : 'cancelled or back'}`) + + if (domainUrl === 'BACK') { + // User wants to go back to authentication method selection + logger.debug('User chose to go back from domain URL input') + return { status: 'BACK' } + } + + if (!domainUrl) { + // User cancelled + logger.debug('User cancelled domain URL input') + throw new ToolkitError('User cancelled domain URL input', { + cancelled: true, + code: SmusErrorCodes.UserCancelled, + }) + } + + try { + // Connect to SMUS using the authentication provider + const connection = await authProvider.connectToSmusWithSso(domainUrl) + + if (!connection) { + throw new ToolkitError('Failed to establish connection', { + code: SmusErrorCodes.FailedAuthConnecton, + }) + } + + // Extract domain account ID, domain ID, and region for logging + const domainId = connection.domainId + const region = authProvider.getDomainRegion() // Use the auth provider method that handles both connection types + + logger.info(`Connected to SageMaker Unified Studio domain: ${domainId} in region ${region}`) + await this.recordAuthTelemetry(span, authProvider, domainId, region) + + // Ask to remember authentication method preference + await this.askToRememberAuthMethod(context, 'sso') + + // Immediately refresh the tree view to show authenticated state + try { + await vscode.commands.executeCommand('aws.smus.rootView.refresh') + } catch (refreshErr) { + logger.debug(`Failed to refresh views after login: ${(refreshErr as Error).message}`) + } + + return { status: 'SUCCESS' } + } catch (connectionErr) { + // Clear the status bar message + vscode.window.setStatusBarMessage('Connection to SageMaker Unified Studio Failed') + + // Log the error and re-throw to be handled by the outer catch block + logger.error('Connection failed: %s', (connectionErr as Error).message) + throw new ToolkitError('Connection failed.', { + cause: connectionErr as Error, + code: (connectionErr as Error).name, + }) + } + } + + /** + * Asks the user if they want to remember their authentication method choice after successful login + */ + private static async askToRememberAuthMethod( + context: vscode.ExtensionContext, + method: SmusAuthenticationMethod + ): Promise { + const logger = this.logger + + try { + const methodName = method === 'sso' ? 'SSO Authentication' : 'IAM Credential Profile' + + const result = await vscode.window.showInformationMessage( + `Remember ${methodName} as your preferred authentication method for SageMaker Unified Studio?`, + 'Yes', + 'No' + ) + + if (result === 'Yes') { + logger.debug(`Saving user preference: ${method}`) + await SmusAuthenticationPreferencesManager.setPreferredMethod(context, method, true) + logger.debug(`Preference saved successfully`) + } + } catch (error) { + // Not a hard failure, so not throwing error + logger.warn('Error asking to remember auth method: %s', error) + } + } + + /** + * Finds SMUS IAM-based domain using IAM credentials + * @param authProvider The SMUS authentication provider + * @param profileName The AWS credential profile name + * @param region The AWS region + * @returns Promise resolving to domain URL or undefined if no IAM-based domain found + */ + private static async findSmusIamDomain( + authProvider: SmusAuthenticationProvider, + profileName: string, + region: string + ): Promise { + const logger = this.logger + + try { + logger.debug(`Finding IAM-based domain in region ${region} using profile ${profileName}`) + + // Get DataZoneCustomClientHelper instance + const datazoneCustomClientHelper = DataZoneCustomClientHelper.getInstance( + await authProvider.getCredentialsProviderForIamProfile(profileName), + region + ) + + // Find the IAM-based domain using the client + const iamDomain = await datazoneCustomClientHelper.getIamDomain() + + if (!iamDomain) { + logger.warn(`No IAM-based domain found in region ${region}`) + return undefined + } + + logger.debug(`Found IAM-based domain: ${iamDomain.name} (${iamDomain.id})`) + + // Construct domain URL from the IAM-based domain + const domainUrl = iamDomain.portalUrl || `https://${iamDomain.id}.sagemaker.${region}.on.aws/` + logger.info(`Discovered IAM-based domain URL: ${domainUrl}`) + + return domainUrl + } catch (error) { + logger.error(`Failed to find IAM-based domain: %s`, error) + throw new ToolkitError(`Failed to find IAM-based domain: ${(error as Error).message}`, { + code: SmusErrorCodes.ApiTimeout, + cause: error instanceof Error ? error : undefined, + }) + } + } + + /** + * Records authentication telemetry + */ + private static async recordAuthTelemetry( + span: any, + authProvider: SmusAuthenticationProvider, + domainId: string, + region: string + ): Promise { + await recordAuthTelemetry(span, authProvider, domainId, region) + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/credentialExpiryHandler.ts b/packages/core/src/sagemakerunifiedstudio/auth/credentialExpiryHandler.ts new file mode 100644 index 00000000000..3c03b2875e4 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/credentialExpiryHandler.ts @@ -0,0 +1,241 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../shared/logger/logger' +import { ToolkitError } from '../../shared/errors' +import { SmusErrorCodes } from '../shared/smusUtils' +import { SmusIamProfileSelector } from './ui/iamProfileSelection' +import { getCredentialsFilename, getConfigFilename } from '../../auth/credentials/sharedCredentialsFile' +import type { SmusAuthenticationProvider } from './providers/smusAuthenticationProvider' + +export enum IamCredentialExpiryAction { + Reauthenticate = 'reauthenticate', + EditCredentials = 'editCredentials', + SwitchProfile = 'switchProfile', + SignOut = 'signOut', + Cancelled = 'cancelled', +} + +export type IamCredentialExpiryResult = + | { action: IamCredentialExpiryAction.Reauthenticate } + | { action: IamCredentialExpiryAction.EditCredentials } + | { action: IamCredentialExpiryAction.SwitchProfile } + | { action: IamCredentialExpiryAction.SignOut } + | { action: IamCredentialExpiryAction.Cancelled } + +/** + * Shows credential expiry options for IAM connections + * Provides options to re-authenticate, edit credentials, switch profiles, or sign out + * @param authProvider The SMUS authentication provider + * @param connection The expired IAM connection + * @param extensionContext The extension context + * @returns Promise that resolves with the action taken + */ +export async function showIamCredentialExpiryOptions( + authProvider: SmusAuthenticationProvider, + connection: any, + extensionContext: vscode.ExtensionContext +): Promise { + const logger = getLogger('smus') + + type QuickPickItemWithAction = vscode.QuickPickItem & { action: IamCredentialExpiryAction } + const options: QuickPickItemWithAction[] = [ + { + label: '$(sync) Re-authenticate with current profile', + description: `Profile: ${connection.profileName}`, + detail: 'Refresh credentials using the same IAM profile', + action: IamCredentialExpiryAction.Reauthenticate, + }, + { + label: '$(file-text) Edit credentials file', + description: 'Open ~/.aws/credentials and ~/.aws/config', + detail: 'Manually update your AWS credentials', + action: IamCredentialExpiryAction.EditCredentials, + }, + { + label: '$(arrow-swap) Switch to another profile', + description: 'Select a different IAM profile', + detail: 'Choose from available credential profiles', + action: IamCredentialExpiryAction.SwitchProfile, + }, + { + label: '$(trash) Sign out', + description: 'Sign out from this connection', + detail: 'Remove the expired connection', + action: IamCredentialExpiryAction.SignOut, + }, + ] + + const quickPick = vscode.window.createQuickPick() + quickPick.title = 'IAM Credentials Expired' + quickPick.placeholder = 'Choose how to fix your expired credentials' + quickPick.items = options + quickPick.canSelectMany = false + quickPick.ignoreFocusOut = true + + return new Promise((resolve, reject) => { + let isCompleted = false + + quickPick.onDidAccept(async () => { + const selectedItem = quickPick.selectedItems[0] + if (!selectedItem) { + quickPick.dispose() + reject(new ToolkitError('No option selected', { code: SmusErrorCodes.UserCancelled, cancelled: true })) + return + } + + isCompleted = true + quickPick.dispose() + + const itemWithAction = selectedItem as QuickPickItemWithAction + + try { + switch (itemWithAction.action) { + case IamCredentialExpiryAction.Reauthenticate: { + logger.debug( + `SMUS: Re-authenticating with current IAM profile: ${connection.profileName} in region ${connection.region}` + ) + // For IAM connections, just validate the credentials are still valid + // The auth system will handle refreshing them automatically + const validation = await authProvider.validateIamProfile(connection.profileName) + if (validation.isValid) { + // Credentials are valid, refresh the connection state + await authProvider.auth.refreshConnectionState(connection) + void vscode.window.showInformationMessage( + 'Successfully reauthenticated with SageMaker Unified Studio' + ) + resolve({ action: IamCredentialExpiryAction.Reauthenticate }) + } else { + const errorMsg = validation.error || 'Unknown validation error' + // Throw error for telemetry - activation.ts will show the notification + throw new ToolkitError( + `Failed to re-authenticate, ensure credential has been updated: ${errorMsg}`, + { code: SmusErrorCodes.IamValidationFailed } + ) + } + break + } + case IamCredentialExpiryAction.EditCredentials: { + logger.debug('Opening AWS credentials and config files for editing') + // Open both credentials and config files like AWS Explorer does + const credentialsPath = getCredentialsFilename() + const configPath = getConfigFilename() + + // Open both files + const [credentialsDoc, configDoc] = await Promise.all([ + vscode.workspace.openTextDocument(credentialsPath), + vscode.workspace.openTextDocument(configPath), + ]) + + // Show both documents + await vscode.window.showTextDocument(credentialsDoc, { preview: false }) + await vscode.window.showTextDocument(configDoc, { + preview: false, + viewColumn: vscode.ViewColumn.Beside, + }) + + void vscode.window.showInformationMessage( + 'AWS credentials and config files opened. Please update your credentials and try reconnecting.' + ) + resolve({ action: IamCredentialExpiryAction.EditCredentials }) + break + } + case IamCredentialExpiryAction.SwitchProfile: { + logger.debug('Switching to another IAM profile') + try { + const profileSelection = await SmusIamProfileSelector.showIamProfileSelection() + + // Handle back navigation - show the credential expiry menu again + if ('isBack' in profileSelection) { + logger.debug('User clicked back, showing credential expiry options again') + // Recursively show the credential expiry options menu + const result = await showIamCredentialExpiryOptions( + authProvider, + connection, + extensionContext + ) + resolve(result) + return + } + + // Handle editing mode - This is if user picks edit during the profile selection + if ('isEditing' in profileSelection) { + logger.debug('User is editing credentials') + resolve({ action: IamCredentialExpiryAction.EditCredentials }) + return + } + + // User selected a new profile, authenticate with it using the selected profile + // Use dynamic import to avoid circular dependency + const { SmusAuthenticationOrchestrator } = await import('./authenticationOrchestrator.js') + const result = await SmusAuthenticationOrchestrator.handleIamAuthentication( + authProvider, + { record: () => {} }, // Minimal span object + extensionContext, + profileSelection.profileName, + profileSelection.region + ) + + if (result.status === 'SUCCESS') { + void vscode.window.showInformationMessage( + `Successfully switched to profile: ${profileSelection.profileName}` + ) + resolve({ action: IamCredentialExpiryAction.SwitchProfile }) + } else if (result.status === 'INVALID_PROFILE') { + void vscode.window.showErrorMessage(`Failed to switch profile: ${result.error}`) + resolve({ action: IamCredentialExpiryAction.SwitchProfile }) + } else { + // BACK or EDITING - shouldn't happen here but handle gracefully + resolve({ action: IamCredentialExpiryAction.Cancelled }) + } + } catch (switchError) { + // Handle user cancellation gracefully + if ( + switchError instanceof ToolkitError && + switchError.code === SmusErrorCodes.UserCancelled + ) { + logger.debug('Profile switch cancelled by user') + resolve({ action: IamCredentialExpiryAction.Cancelled }) + } else { + // Show error message for actual failures + const errorMsg = (switchError as Error).message + void vscode.window.showErrorMessage(`Failed to switch profile: ${errorMsg}`) + logger.error('Profile switch failed: %s', switchError) + resolve({ action: IamCredentialExpiryAction.SwitchProfile }) + } + } + break + } + case IamCredentialExpiryAction.SignOut: { + logger.debug('Signing out from connection') + // Use the provider's signOut method which properly handles metadata cleanup + await authProvider.signOut() + void vscode.window.showInformationMessage('Successfully signed out') + resolve({ action: IamCredentialExpiryAction.SignOut }) + break + } + } + } catch (error) { + logger.error('Failed to handle credential expiry action: %s', error) + // Only show error for non-reauthenticate cases (reauthenticate handles its own errors) + if (itemWithAction.action !== IamCredentialExpiryAction.Reauthenticate) { + void vscode.window.showErrorMessage(`Failed to complete action: ${(error as Error).message}`) + } + reject(error) + } + }) + + quickPick.onDidHide(() => { + if (!isCompleted) { + quickPick.dispose() + logger.debug('Credential expiry options cancelled by user') + resolve({ action: IamCredentialExpiryAction.Cancelled }) + } + }) + + quickPick.show() + }) +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/model.ts b/packages/core/src/sagemakerunifiedstudio/auth/model.ts new file mode 100644 index 00000000000..adc6fd29455 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/model.ts @@ -0,0 +1,147 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SsoProfile, SsoConnection, Connection, IamConnection } from '../../auth/connection' +import { DevSettings } from '../../shared/settings' + +/** + * Default scope for SageMaker Unified Studio authentication + */ +export const scopeSmus = 'datazone:domain:access' + +/** + * Gets the DataZone SSO scope from user settings or returns the default + */ +export function getDataZoneSsoScope(): string { + const devSettings = DevSettings.instance + return devSettings.get('datazoneScope', scopeSmus) +} + +/** + * SageMaker Unified Studio profile extending the base SSO profile + */ +export interface SmusSsoProfile extends SsoProfile { + readonly domainUrl: string + readonly domainId: string +} + +/** + * SageMaker Unified Studio SSO connection extending the base SSO connection + */ +export interface SmusSsoConnection extends SmusSsoProfile, SsoConnection { + readonly id: string + readonly label: string +} + +/** + * SageMaker Unified Studio IAM connection for credential profile authentication + */ +export interface SmusIamConnection extends IamConnection { + readonly profileName: string + readonly region: string + readonly domainUrl: string + readonly domainId: string +} + +/** + * Union type for all SMUS connection types (SSO and IAM) + */ +export type SmusConnection = SmusSsoConnection | SmusIamConnection + +/** + * Creates a SageMaker Unified Studio profile + * @param domainUrl The SageMaker Unified Studio domain URL + * @param domainId The SageMaker Unified Studio domain ID + * @param startUrl The SSO start URL (issuer URL) + * @param region The AWS region + * @returns A SageMaker Unified Studio profile + */ +export function createSmusProfile( + domainUrl: string, + domainId: string, + startUrl: string, + region: string, + scopes = [getDataZoneSsoScope()] +): SmusSsoProfile & { readonly scopes: string[] } { + return { + scopes, + type: 'sso', + startUrl, + ssoRegion: region, + domainUrl, + domainId, + } +} + +/** + * Type guard to check if a connection is a SMUS IAM connection + * @param conn Connection to check + * @returns True if the connection is a SMUS IAM connection + */ +export function isSmusIamConnection(conn?: Connection): conn is SmusIamConnection { + return !!( + conn && + conn.type === 'iam' && + 'profileName' in conn && + 'region' in conn && + 'domainId' in conn && + typeof conn.profileName === 'string' && + typeof conn.region === 'string' && + typeof conn.domainId === 'string' + ) +} + +/** + * Type guard to check if a connection is a SMUS SSO connection + * @param conn Connection to check + * @returns True if the connection is a SMUS SSO connection + */ +export function isSmusSsoConnection(conn?: Connection): conn is SmusSsoConnection { + if (!conn || conn.type !== 'sso') { + return false + } + // Check if the connection has the required SMUS scope (check both default and custom scope) + const configuredScope = getDataZoneSsoScope() + const hasScope = + Array.isArray((conn as any).scopes) && + ((conn as any).scopes.includes(scopeSmus) || (conn as any).scopes.includes(configuredScope)) + // Check if the connection has the required SMUS properties + const hasSmusProps = 'domainUrl' in conn && 'domainId' in conn + return !!hasScope && !!hasSmusProps +} + +/** + * Checks if a connection is a valid SageMaker Unified Studio connection (either SSO or IAM) + * @param conn Connection to check + * @param smusMetadata Optional SMUS metadata for IAM connections + * @returns True if the connection is a valid SMUS connection + */ +export function isValidSmusConnection(conn?: any, smusMetadata?: any): conn is SmusConnection | IamConnection { + // Accept SMUS SSO connections + if (isSmusSsoConnection(conn)) { + return true + } + + // For IAM connections, check if they have SMUS metadata either in the connection or separately + if (conn && conn.type === 'iam') { + // Check if connection already has SMUS properties + if (isSmusIamConnection(conn)) { + return true + } + + // Check if we have separate SMUS metadata for this IAM connection + if ( + smusMetadata && + typeof smusMetadata.profileName === 'string' && + typeof smusMetadata.region === 'string' && + typeof smusMetadata.domainUrl === 'string' && + typeof smusMetadata.domainId === 'string' + ) { + return true + } + } + + return false +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/preferences/authenticationPreferences.ts b/packages/core/src/sagemakerunifiedstudio/auth/preferences/authenticationPreferences.ts new file mode 100644 index 00000000000..47416e73250 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/preferences/authenticationPreferences.ts @@ -0,0 +1,191 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../../shared/logger/logger' +import globals from '../../../shared/extensionGlobals.js' +import { SmusAuthenticationMethod } from '../ui/authenticationMethodSelection.js' + +/** + * Configuration for IAM profile preferences + */ +export interface SmusIamProfileConfig { + profileName: string + region: string + lastUsed?: Date + isDefault?: boolean +} + +/** + * SMUS authentication preferences + */ +export interface SmusAuthenticationPreferences { + preferredMethod?: SmusAuthenticationMethod + lastUsedSsoConnection?: string + lastUsedIamProfile?: SmusIamProfileConfig + rememberChoice: boolean +} + +/** + * Manager for SMUS authentication preferences + */ +export class SmusAuthenticationPreferencesManager { + private static readonly logger = getLogger('smus') + // eslint-disable-next-line @typescript-eslint/naming-convention + private static readonly PREFERENCES_KEY = 'aws.smus.authenticationPreferences' + + /** + * Gets the current authentication preferences + * @param context VS Code extension context (unused, kept for API compatibility) + * @returns Current authentication preferences + */ + public static getPreferences(context?: vscode.ExtensionContext): SmusAuthenticationPreferences { + const stored = globals.globalState.get(this.PREFERENCES_KEY) + + return { + rememberChoice: false, + ...stored, + } + } + + /** + * Updates authentication preferences + * @param context VS Code extension context (unused, kept for API compatibility) + * @param preferences Preferences to update + */ + public static async updatePreferences( + context: vscode.ExtensionContext, + preferences: Partial + ): Promise { + const logger = this.logger + + const current = this.getPreferences() + const updated = { ...current, ...preferences } + + logger.debug( + `SMUS Auth: Updating authentication preferences - preferredMethod: ${updated.preferredMethod}, rememberChoice: ${updated.rememberChoice}` + ) + + await globals.globalState.update(this.PREFERENCES_KEY, updated) + } + + /** + * Sets the preferred authentication method + * @param context VS Code extension context + * @param method Preferred authentication method + * @param rememberChoice Whether to remember this choice + */ + public static async setPreferredMethod( + context: vscode.ExtensionContext, + method: SmusAuthenticationMethod, + rememberChoice: boolean + ): Promise { + await this.updatePreferences(context, { + preferredMethod: method, + rememberChoice, + }) + } + + /** + * Gets the preferred authentication method + * @param context VS Code extension context (unused, kept for API compatibility) + * @returns Preferred authentication method or undefined if not set + */ + public static getPreferredMethod(context?: vscode.ExtensionContext): SmusAuthenticationMethod | undefined { + const preferences = this.getPreferences() + return preferences.rememberChoice ? preferences.preferredMethod : undefined + } + + /** + * Sets the last used SSO connection + * @param context VS Code extension context + * @param connectionId Connection ID + */ + public static async setLastUsedSsoConnection( + context: vscode.ExtensionContext, + connectionId: string + ): Promise { + await this.updatePreferences(context, { + lastUsedSsoConnection: connectionId, + }) + } + + /** + * Sets the last used IAM profile configuration + * @param context VS Code extension context + * @param profileConfig IAM profile configuration + */ + public static async setLastUsedIamProfile( + context: vscode.ExtensionContext, + profileConfig: SmusIamProfileConfig + ): Promise { + await this.updatePreferences(context, { + lastUsedIamProfile: { + ...profileConfig, + lastUsed: new Date(), + }, + }) + } + + /** + * Gets the last used IAM profile configuration + * @param context VS Code extension context (unused, kept for API compatibility) + * @returns Last used IAM profile configuration or undefined + */ + public static getLastUsedIamProfile(context?: vscode.ExtensionContext): SmusIamProfileConfig | undefined { + const preferences = this.getPreferences() + return preferences.lastUsedIamProfile + } + + /** + * Clears all authentication preferences + * @param context VS Code extension context (unused, kept for API compatibility) + */ + public static async clearPreferences(context?: vscode.ExtensionContext): Promise { + const logger = this.logger + logger.debug('Clearing authentication preferences') + + await globals.globalState.update(this.PREFERENCES_KEY, undefined) + } + + /** + * Clears only connection-specific preferences, preserving authentication method preference + * @param context VS Code extension context (unused, kept for API compatibility) + */ + public static async clearConnectionPreferences(context?: vscode.ExtensionContext): Promise { + const logger = this.logger + logger.debug('Clearing connection-specific preferences (preserving auth method preference)') + + const currentPrefs = this.getPreferences() + + // Keep only the authentication method preference and rememberChoice flag + const preservedPrefs: SmusAuthenticationPreferences = { + preferredMethod: currentPrefs.preferredMethod, + rememberChoice: currentPrefs.rememberChoice, + // Clear connection-specific data + lastUsedSsoConnection: undefined, + lastUsedIamProfile: undefined, + } + + await globals.globalState.update(this.PREFERENCES_KEY, preservedPrefs) + } + + /** + * Switches the authentication method preference + * @param context VS Code extension context + * @param newMethod New authentication method to switch to + */ + public static async switchAuthenticationMethod( + context: vscode.ExtensionContext, + newMethod: SmusAuthenticationMethod + ): Promise { + const logger = this.logger + logger.debug(`Switching authentication method to: ${newMethod}`) + + await this.updatePreferences(context, { + preferredMethod: newMethod, + }) + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider.ts b/packages/core/src/sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider.ts new file mode 100644 index 00000000000..30b8477f6cb --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider.ts @@ -0,0 +1,252 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLogger } from '../../../shared/logger/logger' +import { ToolkitError } from '../../../shared/errors' +import * as AWS from '@aws-sdk/types' +import { CredentialsId, CredentialsProvider, CredentialsProviderType } from '../../../auth/providers/credentials' + +import { DataZoneClient } from '../../shared/client/datazoneClient' +import { SmusAuthenticationProvider } from './smusAuthenticationProvider' +import { CredentialType } from '../../../shared/telemetry/telemetry' +import { SmusCredentialExpiry, validateCredentialFields } from '../../shared/smusUtils' +import { getContext } from '../../../shared/vscode/setContext' + +/** + * Credentials provider for SageMaker Unified Studio Connection credentials + * Uses DataZone API to get connection credentials for a specific connection * + * This provider implements independent caching with 10-minute expiry + */ +export class ConnectionCredentialsProvider implements CredentialsProvider { + private readonly logger = getLogger('smus') + private credentialCache?: { + credentials: AWS.Credentials + expiresAt: Date + } + + constructor( + private readonly smusAuthProvider: SmusAuthenticationProvider, + private readonly connectionId: string, + private readonly projectId: string + ) {} + + /** + * Gets the connection ID + * @returns Connection ID + */ + public getConnectionId(): string { + return this.connectionId + } + + /** + * Gets the credentials ID + * @returns Credentials ID + */ + public getCredentialsId(): CredentialsId { + return { + credentialSource: 'temp', + credentialTypeId: `${this.smusAuthProvider.getDomainId()}:${this.connectionId}`, + } + } + + /** + * Gets the provider type + * @returns Provider type + */ + public getProviderType(): CredentialsProviderType { + return 'temp' + } + + /** + * Gets the telemetry type + * @returns Telemetry type + */ + public getTelemetryType(): CredentialType { + return 'other' + } + + /** + * Gets the default region + * @returns Default region + */ + public getDefaultRegion(): string | undefined { + return this.smusAuthProvider.getDomainRegion() + } + + /** + * Gets the domain AWS account ID + * @returns Promise resolving to the domain account ID + */ + public async getDomainAccountId(): Promise { + return this.smusAuthProvider.getDomainAccountId() + } + + /** + * Gets the hash code + * @returns Hash code + */ + public getHashCode(): string { + const hashCode = `smus-connection:${this.smusAuthProvider.getDomainId()}:${this.connectionId}` + return hashCode + } + + /** + * Determines if the provider can auto-connect + * @returns Promise resolving to boolean + */ + public async canAutoConnect(): Promise { + return false // SMUS requires manual authentication + } + + /** + * Determines if the provider is available + * @returns Promise resolving to boolean + */ + public async isAvailable(): Promise { + try { + return this.smusAuthProvider.isConnected() + } catch (err) { + this.logger.error('Error checking if auth provider is connected: %s', err) + return false + } + } + + /** + * Gets Connection credentials with independent caching + * @returns Promise resolving to credentials + */ + public async getCredentials(): Promise { + this.logger.debug(`Getting credentials for connection ${this.connectionId}`) + + // Check cache first (10-minute expiry) + if (this.credentialCache && this.credentialCache.expiresAt > new Date()) { + this.logger.debug( + `SMUS Connection: Using cached connection credentials for connection ${this.connectionId}` + ) + return this.credentialCache.credentials + } + + this.logger.debug( + `SMUS Connection: Calling GetConnection to fetch credentials for connection ${this.connectionId}` + ) + + try { + if (getContext('aws.smus.isIamMode') && this.projectId) { + return (await this.smusAuthProvider.getProjectCredentialProvider(this.projectId)).getCredentials() + } + const datazoneClient = DataZoneClient.createWithCredentials( + this.smusAuthProvider.getDomainRegion(), + this.smusAuthProvider.getDomainId(), + await this.smusAuthProvider.getDerCredentialsProvider() + ) + const getConnectionResponse = await datazoneClient.getConnection({ + domainIdentifier: this.smusAuthProvider.getDomainId(), + identifier: this.connectionId, + withSecret: true, + }) + + this.logger.debug(`Successfully retrieved connection details for ${this.connectionId}`) + + // Extract connection credentials + const connectionCredentials = getConnectionResponse.connectionCredentials + if (!connectionCredentials) { + throw new ToolkitError( + `No connection credentials available in response for connection ${this.connectionId}`, + { + code: 'NoConnectionCredentials', + } + ) + } + + // Validate credential fields + validateCredentialFields( + connectionCredentials, + 'InvalidConnectionCredentials', + 'connection credential response', + true + ) + + // Create AWS credentials with expiration + // Use the expiration from the response if available, otherwise default to 10 minutes + let expiresAt: Date + if (connectionCredentials.expiration) { + // The API returns expiration as a string or Date, handle both cases + expiresAt = + connectionCredentials.expiration instanceof Date + ? connectionCredentials.expiration + : new Date(connectionCredentials.expiration) + } else { + expiresAt = new Date(Date.now() + SmusCredentialExpiry.connectionExpiryMs) + } + + const awsCredentials: AWS.Credentials = { + accessKeyId: connectionCredentials.accessKeyId as string, + secretAccessKey: connectionCredentials.secretAccessKey as string, + sessionToken: connectionCredentials.sessionToken as string, + expiration: expiresAt, + } + + // Cache connection credentials (10-minute expiry) + const cacheExpiresAt = new Date(Date.now() + SmusCredentialExpiry.connectionExpiryMs) + this.credentialCache = { + credentials: awsCredentials, + expiresAt: cacheExpiresAt, + } + + this.logger.debug( + `SMUS Connection: Successfully cached connection credentials for connection ${this.connectionId}, expires in %s minutes`, + Math.round((cacheExpiresAt.getTime() - Date.now()) / 60000) + ) + + return awsCredentials + } catch (err) { + this.logger.error( + `SMUS Connection: Failed to get connection credentials for connection ${this.connectionId}: %s`, + err + ) + + // Re-throw ToolkitErrors with specific codes (NoConnectionCredentials, InvalidConnectionCredentials) + if ( + err instanceof ToolkitError && + (err.code === 'NoConnectionCredentials' || err.code === 'InvalidConnectionCredentials') + ) { + throw err + } + + // Wrap other errors in ConnectionCredentialsFetchFailed + throw new ToolkitError(`Failed to get connection credentials for ${this.connectionId}: ${err}`, { + code: 'ConnectionCredentialsFetchFailed', + cause: err instanceof Error ? err : undefined, + }) + } + } + + /** + * Invalidates cached connection credentials + * Clears the internal cache without fetching new credentials + */ + public invalidate(): void { + this.logger.debug(`Invalidating cached credentials for connection ${this.connectionId}`) + // Clear cache to force fresh fetch on next getCredentials() call + this.credentialCache = undefined + this.logger.debug( + `SMUS Connection: Successfully invalidated connection credentials cache for connection ${this.connectionId}` + ) + } + + /** + * Disposes of the provider and cleans up resources + */ + public dispose(): void { + this.logger.debug( + `SMUS Connection: Disposing connection credentials provider for connection ${this.connectionId}` + ) + // Clear cache to clean up resources + this.invalidate() + this.logger.debug( + `SMUS Connection: Successfully disposed connection credentials provider for connection ${this.connectionId}` + ) + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/providers/domainExecRoleCredentialsProvider.ts b/packages/core/src/sagemakerunifiedstudio/auth/providers/domainExecRoleCredentialsProvider.ts new file mode 100644 index 00000000000..85da4901a94 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/providers/domainExecRoleCredentialsProvider.ts @@ -0,0 +1,325 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLogger } from '../../../shared/logger/logger' +import { ToolkitError } from '../../../shared/errors' +import * as AWS from '@aws-sdk/types' +import { CredentialsId, CredentialsProvider, CredentialsProviderType } from '../../../auth/providers/credentials' +import fetch from 'node-fetch' +import globals from '../../../shared/extensionGlobals' +import { CredentialType } from '../../../shared/telemetry/telemetry' +import { SmusCredentialExpiry, SmusTimeouts, SmusErrorCodes, validateCredentialFields } from '../../shared/smusUtils' + +/** + * Credentials provider for SageMaker Unified Studio Domain Execution Role (DER) + * Uses SSO tokens to get DER credentials via the /sso/redeem-token endpoint + * + * This provider implements internal caching with 10-minute expiry and handles + * its own credential lifecycle independently + */ +export class DomainExecRoleCredentialsProvider implements CredentialsProvider { + private readonly logger = getLogger('smus') + private credentialCache?: { + credentials: AWS.Credentials + expiresAt: Date + } + + constructor( + private readonly domainUrl: string, + private readonly domainId: string, + private readonly ssoRegion: string, + private readonly getAccessToken: () => Promise // Function to get SSO access token for the Connection + ) {} + + /** + * Gets the domain ID + * @returns Domain ID + */ + public getDomainId(): string { + return this.domainId + } + + /** + * Gets the domain URL + * @returns Domain URL + */ + public getDomainUrl(): string { + return this.domainUrl + } + + /** + * Gets the credentials ID + * @returns Credentials ID + */ + public getCredentialsId(): CredentialsId { + return { + credentialSource: 'sso', + credentialTypeId: this.domainId, + } + } + + /** + * Gets the provider type + * @returns Provider type + */ + public getProviderType(): CredentialsProviderType { + return 'sso' + } + + /** + * Gets the telemetry type + * @returns Telemetry type + */ + public getTelemetryType(): CredentialType { + return 'ssoProfile' + } + + /** + * Gets the default region + * @returns Default region + */ + public getDefaultRegion(): string | undefined { + return this.ssoRegion + } + + /** + * Gets the hash code + * @returns Hash code + */ + public getHashCode(): string { + const hashCode = `smus-der:${this.domainId}:${this.ssoRegion}` + return hashCode + } + + /** + * Determines if the provider can auto-connect + * @returns Promise resolving to boolean + */ + public async canAutoConnect(): Promise { + return false // SMUS requires manual authentication + } + + /** + * Determines if the provider is available + * @returns Promise resolving to boolean + */ + public async isAvailable(): Promise { + try { + // Check if we can get an access token + await this.getAccessToken() + return true + } catch { + return false + } + } + + /** + * Gets Domain Execution Role (DER) credentials with internal caching + * @returns Promise resolving to credentials + */ + public async getCredentials(): Promise { + this.logger.debug(`Getting DER credentials for domain ${this.domainId}`) + + // Check cache first (10-minute expiry with 5-minute buffer for proactive refresh) + if (this.credentialCache && this.credentialCache.expiresAt > new Date()) { + this.logger.debug(`Using cached DER credentials for domain ${this.domainId}`) + return this.credentialCache.credentials + } + + this.logger.debug(`Fetching credentials from API for domain ${this.domainId}`) + + try { + // Get current SSO access token + const accessToken = await this.getAccessToken() + if (!accessToken) { + throw new ToolkitError('No access token available for DER credential refresh', { + code: 'NoTokenAvailable', + }) + } + + this.logger.debug(`Got access token for refresh for domain ${this.domainId}`) + + // Call SMUS redeem token API to get DER credentials + const redeemUrl = new URL('/sso/redeem-token', this.domainUrl) + this.logger.debug(`Calling redeem token endpoint: ${redeemUrl.toString()}`) + + const requestBody = { + domainId: this.domainId, + accessToken, + } + + const requestHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'aws-toolkit-vscode', + } + + let response + try { + response = await fetch(redeemUrl.toString(), { + method: 'POST', + headers: requestHeaders, + body: JSON.stringify(requestBody), + timeout: SmusTimeouts.apiCallTimeoutMs, + }) + } catch (fetchError) { + // Handle timeout errors specifically + if ( + fetchError instanceof Error && + (fetchError.name === 'AbortError' || fetchError.message.includes('timeout')) + ) { + throw new ToolkitError( + `Redeem token request timed out after ${SmusTimeouts.apiCallTimeoutMs / 1000} seconds`, + { + code: SmusErrorCodes.ApiTimeout, + cause: fetchError, + } + ) + } + // Re-throw other fetch errors + throw fetchError + } + + this.logger.debug(`Redeem token response status: ${response.status} for domain ${this.domainId}`) + + if (!response.ok) { + // Try to get response body for more details + let responseBody = '' + try { + responseBody = await response.text() + this.logger.debug(`Error response body for domain ${this.domainId}: ${responseBody}`) + } catch (bodyErr) { + this.logger.debug( + `SMUS DER: Could not read error response body for domain ${this.domainId}: ${bodyErr}` + ) + } + + throw new ToolkitError( + `Failed to redeem access token: ${response.status} ${response.statusText}${responseBody ? ` - ${responseBody}` : ''}`, + { code: SmusErrorCodes.RedeemAccessTokenFailed } + ) + } + + const responseText = await response.text() + + const data = JSON.parse(responseText) as { + credentials: { + accessKeyId: string + secretAccessKey: string + sessionToken: string + expiration: string + } + } + this.logger.debug(`Successfully received credentials from API for domain ${this.domainId}`) + + // Validate the response data structure + if (!data.credentials) { + throw new ToolkitError('Missing credentials object in API response', { + code: 'InvalidCredentialResponse', + }) + } + + const credentials = data.credentials + + // Validate the credential fields + validateCredentialFields(credentials, 'InvalidCredentialResponse', 'API response') + + // Create credentials with expiration + let credentialExpiresAt: Date + if (credentials.expiration) { + // Handle both epoch timestamps and ISO date strings + let parsedExpiration: Date + + // Check if expiration is a numeric string (epoch timestamp) + const expirationNum = Number(credentials.expiration) + if (!isNaN(expirationNum) && expirationNum > 0) { + // Treat as epoch timestamp in seconds and convert to milliseconds + const timestampMs = expirationNum * 1000 + parsedExpiration = new Date(timestampMs) + this.logger.debug( + `SMUS DER: Parsed epoch timestamp ${credentials.expiration} (seconds) as ${parsedExpiration.toISOString()}` + ) + } else { + // Treat as ISO date string + parsedExpiration = new Date(credentials.expiration) + if (!isNaN(parsedExpiration.getTime())) { + this.logger.debug( + `SMUS DER: Parsed ISO date string ${credentials.expiration} as ${parsedExpiration.toISOString()}` + ) + } else { + this.logger.debug( + `SMUS DER: Failed to parse ISO date string ${credentials.expiration} - invalid date format` + ) + } + } + + // Check if the parsed date is valid + if (isNaN(parsedExpiration.getTime())) { + this.logger.warn( + `SMUS DER: Invalid expiration value: ${credentials.expiration}, using default expiration` + ) + credentialExpiresAt = new Date(Date.now() + SmusCredentialExpiry.derExpiryMs) + } else { + credentialExpiresAt = parsedExpiration + } + if (!isNaN(credentialExpiresAt.getTime())) { + this.logger.debug(`Credential expires at ${credentialExpiresAt.toISOString()}`) + } else { + this.logger.debug(`Invalid credential expiration date, using default`) + } + } else { + this.logger.debug(`No expiration provided, using default`) + credentialExpiresAt = new Date(Date.now() + SmusCredentialExpiry.derExpiryMs) + } + + const awsCredentials: AWS.Credentials = { + accessKeyId: credentials.accessKeyId as string, + secretAccessKey: credentials.secretAccessKey as string, + sessionToken: credentials.sessionToken as string, + expiration: credentialExpiresAt, + } + + // Cache DER credentials with 10-minute expiry (5-minute buffer for proactive refresh) + const cacheExpiresAt = new globals.clock.Date(Date.now() + SmusCredentialExpiry.derExpiryMs) + this.credentialCache = { + credentials: awsCredentials, + expiresAt: cacheExpiresAt, + } + + this.logger.debug( + 'SMUS DER: Successfully cached DER credentials for domain %s, cache expires in %s minutes', + this.domainId, + Math.round((cacheExpiresAt.getTime() - Date.now()) / 60000) + ) + + return awsCredentials + } catch (err) { + this.logger.error('Failed to fetch credentials for domain %s: %s', this.domainId, err) + throw new ToolkitError(`Failed to fetch DER credentials for domain ${this.domainId}: ${err}`, { + code: 'DerCredentialsFetchFailed', + cause: err instanceof Error ? err : undefined, + }) + } + } + + /** + * Invalidates cached DER credentials + * Clears the internal cache without fetching new credentials + */ + public invalidate(): void { + this.logger.debug(`Invalidating cached DER credentials for domain ${this.domainId}`) + // Clear cache to force fresh fetch on next getCredentials() call + this.credentialCache = undefined + this.logger.debug(`Successfully invalidated DER credentials cache for domain ${this.domainId}`) + } + /** + * Disposes of the provider and cleans up resources + */ + public dispose(): void { + this.logger.debug(`Disposing DER credentials provider for domain ${this.domainId}`) + this.invalidate() + this.logger.debug(`Successfully disposed DER credentials provider for domain ${this.domainId}`) + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/providers/projectRoleCredentialsProvider.ts b/packages/core/src/sagemakerunifiedstudio/auth/providers/projectRoleCredentialsProvider.ts new file mode 100644 index 00000000000..7f67d0a97af --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/providers/projectRoleCredentialsProvider.ts @@ -0,0 +1,363 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLogger } from '../../../shared/logger/logger' +import { ToolkitError } from '../../../shared/errors' +import * as AWS from '@aws-sdk/types' +import { CredentialsId, CredentialsProvider, CredentialsProviderType } from '../../../auth/providers/credentials' + +import { SmusAuthenticationProvider } from './smusAuthenticationProvider' +import { CredentialType } from '../../../shared/telemetry/telemetry' +import { SmusCredentialExpiry, validateCredentialFields } from '../../shared/smusUtils' +import { loadMappings, saveMappings } from '../../../awsService/sagemaker/credentialMapping' +import { createDZClientBaseOnDomainMode } from '../../explorer/nodes/utils' + +/** + * Credentials provider for SageMaker Unified Studio Project Role credentials + * Uses Domain Execution Role (DER) credentials to get project-scoped credentials + * via the DataZone GetEnvironmentCredentials API + * + * This provider implements independent caching with 10-minute expiry and can be used + * with any AWS SDK client (S3Client, LambdaClient, etc.) + */ +export class ProjectRoleCredentialsProvider implements CredentialsProvider { + private readonly logger = getLogger('smus') + private credentialCache?: { + credentials: AWS.Credentials + expiresAt: Date + } + private refreshTimer?: NodeJS.Timeout + private readonly refreshInterval = 10 * 60 * 1000 // 10 minutes + private readonly checkInterval = 10 * 1000 // 10 seconds - check frequently, refresh based on actual time + private sshRefreshActive = false + private lastRefreshTime?: Date + + constructor( + private readonly smusAuthProvider: SmusAuthenticationProvider, + private readonly projectId: string + ) {} + + /** + * Gets the project ID + * @returns Project ID + */ + public getProjectId(): string { + return this.projectId + } + + /** + * Gets the credentials ID + * @returns Credentials ID + */ + public getCredentialsId(): CredentialsId { + return { + credentialSource: 'temp', + credentialTypeId: `${this.smusAuthProvider.getDomainId()}:${this.projectId}`, + } + } + + /** + * Gets the provider type + * @returns Provider type + */ + public getProviderType(): CredentialsProviderType { + return 'temp' + } + + /** + * Gets the telemetry type + * @returns Telemetry type + */ + public getTelemetryType(): CredentialType { + return 'other' + } + + /** + * Gets the default region + * @returns Default region + */ + public getDefaultRegion(): string | undefined { + return this.smusAuthProvider.getDomainRegion() + } + + /** + * Gets the hash code + * @returns Hash code + */ + public getHashCode(): string { + const hashCode = `smus-project:${this.smusAuthProvider.getDomainId()}:${this.projectId}` + return hashCode + } + + /** + * Determines if the provider can auto-connect + * @returns Promise resolving to boolean + */ + public async canAutoConnect(): Promise { + return false // SMUS requires manual authentication + } + + /** + * Determines if the provider is available + * @returns Promise resolving to boolean + */ + public async isAvailable(): Promise { + return this.smusAuthProvider.isConnected() + } + + /** + * Gets Project Role credentials with independent caching + * @returns Promise resolving to credentials + */ + public async getCredentials(): Promise { + this.logger.debug(`Getting credentials for project ${this.projectId}`) + + // Check cache first (10-minute expiry) + if (this.credentialCache && this.credentialCache.expiresAt > new Date()) { + this.logger.debug(`Using cached project credentials for project ${this.projectId}`) + return this.credentialCache.credentials + } + + this.logger.debug(`Fetching project credentials from API for project ${this.projectId}`) + + try { + const dataZoneClient = await createDZClientBaseOnDomainMode(this.smusAuthProvider) + const response = await dataZoneClient.getProjectDefaultEnvironmentCreds(this.projectId) + + this.logger.debug( + `SMUS Project: Successfully received response from GetEnvironmentCredentials API for project ${this.projectId}` + ) + + // Validate credential fields - credentials are returned directly in the response + validateCredentialFields(response, 'InvalidProjectCredentialResponse', 'project credential response') + + // Create AWS credentials with expiration + // Use the expiration from the response if available, otherwise default to 10 minutes + let expiresAt: Date + if (response.expiration) { + // The API returns expiration as a string, parse it to Date + expiresAt = new Date(response.expiration) + } else { + expiresAt = new Date(Date.now() + SmusCredentialExpiry.projectExpiryMs) + } + + const awsCredentials: AWS.Credentials = { + accessKeyId: response.accessKeyId as string, + secretAccessKey: response.secretAccessKey as string, + sessionToken: response.sessionToken as string, + expiration: expiresAt, + } + + // Cache project credentials + this.credentialCache = { + credentials: awsCredentials, + expiresAt: expiresAt, + } + + this.logger.debug( + 'SMUS Project: Successfully cached project credentials for project %s, expires in %s minutes', + this.projectId, + Math.round((expiresAt.getTime() - Date.now()) / 60000) + ) + + // Write project credentials to mapping file to be used by Sagemaker local server for remote connections + await this.writeCredentialsToMapping(awsCredentials) + + return awsCredentials + } catch (err) { + this.logger.error('Failed to get project credentials for project %s: %s', this.projectId, err) + + // Handle InvalidGrantException specially - indicates need for reauthentication + if (err instanceof Error && err.name === 'InvalidGrantException') { + // Invalidate cache when authentication fails + this.invalidate() + throw new ToolkitError( + `Failed to get project credentials for project ${this.projectId}: ${err.message}. Reauthentication required.`, + { + code: 'InvalidRefreshToken', + cause: err, + } + ) + } + + throw new ToolkitError(`Failed to get project credentials for project ${this.projectId}: ${err}`, { + code: 'ProjectCredentialsFetchFailed', + cause: err instanceof Error ? err : undefined, + }) + } + } + + /** + * Writes project credentials to mapping file for local server usage + */ + private async writeCredentialsToMapping(awsCredentials: AWS.Credentials): Promise { + try { + const mapping = await loadMappings() + mapping.smusProjects ??= {} + mapping.smusProjects[this.projectId] = { + accessKey: awsCredentials.accessKeyId, + secret: awsCredentials.secretAccessKey, + token: awsCredentials.sessionToken || '', + } + await saveMappings(mapping) + } catch (err) { + this.logger.warn('Failed to write project credentials to mapping file: %s', err) + } + } + + /** + * Starts proactive credential refresh for SSH connections + * + * Uses an expiry-based approach with safety buffer: + * - Checks every 10 seconds using setTimeout + * - Refreshes when credentials expire within 5 minutes (safety buffer) + * - Falls back to 10-minute time-based refresh if no expiry information available + * - Handles sleep/resume because it uses wall-clock time for expiry checks + * + * This means credentials are refreshed just before they expire, reducing + * unnecessary API calls while ensuring credentials remain valid. + */ + public startProactiveCredentialRefresh(): void { + if (this.sshRefreshActive) { + this.logger.debug(`SSH refresh already active for project ${this.projectId}`) + return + } + + this.logger.info(`Starting SSH credential refresh for project ${this.projectId}`) + this.sshRefreshActive = true + this.lastRefreshTime = new Date() // Initialize refresh time + + // Start the check timer (checks every 10 seconds, refreshes every 10 minutes based on actual time) + this.scheduleNextCheck() + } + + /** + * Stops proactive credential refresh + * Called when SSH connection ends or SMUS disconnects + */ + public stopProactiveCredentialRefresh(): void { + if (!this.sshRefreshActive) { + return + } + + this.logger.info(`Stopping SSH credential refresh for project ${this.projectId}`) + this.sshRefreshActive = false + this.lastRefreshTime = undefined + + // Clean up timer + if (this.refreshTimer) { + clearTimeout(this.refreshTimer) + this.refreshTimer = undefined + } + } + + /** + * Schedules the next credential check (every 10 seconds) + * Refreshes credentials when they expire within 5 minutes (safety buffer) + * Falls back to 10-minute time-based refresh if no expiry information available + * This handles sleep/resume scenarios correctly + */ + private scheduleNextCheck(): void { + if (!this.sshRefreshActive) { + return + } + // Check every 10 seconds, but only refresh every 10 minutes based on actual time elapsed + this.refreshTimer = setTimeout(async () => { + try { + const now = new Date() + // Check if we need to refresh based on actual time elapsed + if (this.shouldPerformRefresh(now)) { + await this.refresh() + } + // Schedule next check if still active + if (this.sshRefreshActive) { + this.scheduleNextCheck() + } + } catch (error) { + this.logger.error( + `SMUS Project: Failed to refresh credentials for project ${this.projectId}: %O`, + error + ) + // Continue trying even if refresh fails. Dispose will handle stopping the refresh. + if (this.sshRefreshActive) { + this.scheduleNextCheck() + } + } + }, this.checkInterval) + } + + /** + * Determines if a credential refresh should be performed based on credential expiration + * This handles sleep/resume scenarios properly and is more efficient than time-based refresh + */ + private shouldPerformRefresh(now: Date): boolean { + if (!this.lastRefreshTime || !this.credentialCache) { + // First refresh or no cached credentials + this.logger.debug(`First refresh - no previous credentials for ${this.projectId}`) + return true + } + + // Check if credentials expire soon (with 5-minute safety buffer) + const safetyBufferMs = 5 * 60 * 1000 // 5 minutes before expiry + const expiryTime = this.credentialCache.credentials.expiration?.getTime() + + if (!expiryTime) { + // No expiry info - fall back to time-based refresh as safety net + const timeSinceLastRefresh = now.getTime() - this.lastRefreshTime.getTime() + const shouldRefresh = timeSinceLastRefresh >= this.refreshInterval + return shouldRefresh + } + + const timeUntilExpiry = expiryTime - now.getTime() + const shouldRefresh = timeUntilExpiry < safetyBufferMs + return shouldRefresh + } + + /** + * Performs credential refresh by invalidating cache and fetching fresh credentials + */ + private async refresh(): Promise { + const now = new Date() + const expiryTime = this.credentialCache?.credentials.expiration?.getTime() + + if (expiryTime) { + const minutesUntilExpiry = Math.round((expiryTime - now.getTime()) / 60000) + this.logger.debug( + `SMUS Project: Refreshing credentials for project ${this.projectId} - expires in ${minutesUntilExpiry} minutes` + ) + } else { + const minutesSinceLastRefresh = this.lastRefreshTime + ? Math.round((now.getTime() - this.lastRefreshTime.getTime()) / 60000) + : 0 + this.logger.debug( + `SMUS Project: Refreshing credentials for project ${this.projectId} - time-based refresh after ${minutesSinceLastRefresh} minutes` + ) + } + + await this.getCredentials() + this.lastRefreshTime = new Date() + } + + /** + * Invalidates cached project credentials + * Clears the internal cache without fetching new credentials + */ + public invalidate(): void { + this.logger.debug(`Invalidating cached credentials for project ${this.projectId}`) + // Clear cache to force fresh fetch on next getCredentials() call + this.credentialCache = undefined + this.logger.debug( + `SMUS Project: Successfully invalidated project credentials cache for project ${this.projectId}` + ) + } + + /** + * Disposes of the provider and cleans up resources + */ + public dispose(): void { + this.stopProactiveCredentialRefresh() + this.invalidate() + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider.ts b/packages/core/src/sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider.ts new file mode 100644 index 00000000000..11b312faf9f --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider.ts @@ -0,0 +1,1542 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { AwsCredentialIdentity } from '@aws-sdk/types' +import { Auth } from '../../../auth/auth' +import { getSecondaryAuth } from '../../../auth/secondaryAuth' +import { ToolkitError } from '../../../shared/errors' +import { withTelemetryContext } from '../../../shared/telemetry/util' +import { SsoConnection } from '../../../auth/connection' +import { showReauthenticateMessage } from '../../../shared/utilities/messages' +import * as localizedText from '../../../shared/localizedText' +import { ToolkitPromptSettings } from '../../../shared/settings' +import { setContext, getContext } from '../../../shared/vscode/setContext' +import { getLogger } from '../../../shared/logger/logger' +import { + SmusUtils, + SmusErrorCodes, + extractAccountIdFromResourceMetadata, + convertToToolkitCredentialProvider, + isIamDomain, +} from '../../shared/smusUtils' +import { + createSmusProfile, + isValidSmusConnection, + SmusConnection, + SmusIamConnection, + isSmusSsoConnection, + isSmusIamConnection, +} from '../model' +import { IamCredentialExpiryAction, showIamCredentialExpiryOptions } from '../credentialExpiryHandler' + +import { DomainExecRoleCredentialsProvider } from './domainExecRoleCredentialsProvider' +import { ProjectRoleCredentialsProvider } from './projectRoleCredentialsProvider' +import { ConnectionCredentialsProvider } from './connectionCredentialsProvider' +import { ConnectionClientStore } from '../../shared/client/connectionClientStore' +import { getResourceMetadata } from '../../shared/utils/resourceMetadataUtils' +import { CredentialsProviderManager } from '../../../auth/providers/credentialsProviderManager' +import { SharedCredentialsProvider } from '../../../auth/providers/sharedCredentialsProvider' +import { CredentialsId, CredentialsProvider } from '../../../auth/providers/credentials' +import globals from '../../../shared/extensionGlobals' +import { fromContainerMetadata, fromIni, fromNodeProviderChain } from '@aws-sdk/credential-providers' +import { randomUUID } from '../../../shared/crypto' +import { DefaultStsClient } from '../../../shared/clients/stsClient' +import { DataZoneCustomClientHelper } from '../../shared/client/datazoneCustomClientHelper' +import { createDZClientBaseOnDomainMode } from '../../explorer/nodes/utils' +import { DataZoneClient } from '../../shared/client/datazoneClient' +import { loadSharedConfigFiles } from '@smithy/shared-ini-file-loader' +import { loadSharedCredentialsProfiles } from '../../../auth/credentials/sharedCredentials' + +/** + * Sets the context variable for SageMaker Unified Studio connection state + * @param isConnected Whether SMUS is connected + */ +export function setSmusConnectedContext(isConnected: boolean): Promise { + return setContext('aws.smus.connected', isConnected) +} + +/** + * Sets the context variable for SMUS space environment state + * @param inSmusSpace Whether we're in SMUS space environment + */ +export function setSmusSpaceEnvironmentContext(inSmusSpace: boolean): Promise { + return setContext('aws.smus.inSmusSpaceEnvironment', inSmusSpace) +} + +/** + * Sets the context variable for SMUS IAM mode state + * @param isIamMode Whether the current domain is in IAM mode + */ +export function setSmusIamModeContext(isIamMode: boolean): Promise { + return setContext('aws.smus.isIamMode', isIamMode) +} +const authClassName = 'SmusAuthenticationProvider' + +/** + * Authentication provider for SageMaker Unified Studio + * Manages authentication state and credentials for SMUS + */ +export class SmusAuthenticationProvider { + private readonly logger = getLogger('smus') + public readonly onDidChangeActiveConnection: vscode.Event + private readonly onDidChangeEmitter = new vscode.EventEmitter() + public readonly onDidChange = this.onDidChangeEmitter.event + private credentialsProviderCache = new Map() + private projectCredentialProvidersCache = new Map() + private connectionCredentialProvidersCache = new Map() + private cachedDomainAccountId: string | undefined + private cachedProjectAccountIds = new Map() + private iamCallerIdentityCache: { arn: string; connectionId: string } | undefined + + public readonly secondaryAuth: ReturnType + + public constructor(public readonly auth = Auth.instance) { + // Create secondaryAuth after the class is constructed so we can reference instance methods + this.secondaryAuth = getSecondaryAuth( + auth, + 'smus', + 'SageMaker Unified Studio', + (conn): conn is SmusConnection => { + // Use auth's state directly since secondaryAuth isn't available yet during initialization + const state = auth.getStateMemento() + const smusConnections = state.get('smus.connections') as any + const savedConnectionId = state.get('smus.savedConnectionId') as string + + // Only accept IAM connections that are currently saved for SMUS + if (conn && conn.type === 'iam') { + // Must be the exact connection that SMUS has saved AND have metadata + return ( + conn.id === savedConnectionId && + smusConnections && + smusConnections[conn.id] && + isValidSmusConnection(conn, smusConnections[conn.id]) + ) + } + + // SSO connections: Check if they have SMUS scope (always SMUS-specific) + if (conn && conn.type === 'sso') { + return isValidSmusConnection(conn) // Checks for SMUS scope + } + + // Reject everything else + return false + } + ) + + // Initialize the event property + this.onDidChangeActiveConnection = this.secondaryAuth.onDidChangeActiveConnection as vscode.Event< + SmusConnection | undefined + > + + // Set up event listeners + this.secondaryAuth.onDidChangeActiveConnection(async () => { + // Stop SSH credential refresh for all projects when connection changes + this.stopAllSshCredentialRefresh() + + // Invalidate any cached credentials for the previous connection + await this.invalidateAllCredentialsInCache() + // Clear credentials provider cache when connection changes + this.credentialsProviderCache.clear() + // Clear project provider cache when connection changes + this.projectCredentialProvidersCache.clear() + // Clear connection provider cache when connection changes + this.connectionCredentialProvidersCache.clear() + // Clear cached domain account ID when connection changes + this.cachedDomainAccountId = undefined + // Clear cached project account IDs when connection changes + this.cachedProjectAccountIds.clear() + // Clear cached IAM caller identity when connection changes + this.clearIamCallerIdentityCache() + // Clear all clients in client store when connection changes + ConnectionClientStore.getInstance().clearAll() + await setSmusConnectedContext(this.isConnected()) + await setSmusSpaceEnvironmentContext(SmusUtils.isInSmusSpaceEnvironment()) + + // Set IAM mode context based on connection metadata + const activeConn = this.activeConnection + if (activeConn && 'type' in activeConn && activeConn.type === 'iam') { + const smusConnections = (this.secondaryAuth.state.get('smus.connections') as any) || {} + const connectionMetadata = smusConnections[activeConn.id] + const isIamDomain = connectionMetadata?.isIamDomain || false + await setSmusIamModeContext(isIamDomain) + } else { + // Clear IAM mode context for non-IAM connections or no connection + await setSmusIamModeContext(false) + } + // Update IAM mode context in SMUS space environment + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + await this.initIamModeContextInSpaceEnvironment() + } + + this.onDidChangeEmitter.fire() + }) + + // Set initial context in case event does not trigger + void setSmusConnectedContext(this.isConnectionValid()) + void setSmusSpaceEnvironmentContext(SmusUtils.isInSmusSpaceEnvironment()) + + // Set initial IAM mode context + void (async () => { + // Update IAM mode context in SMUS space environment + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + await this.initIamModeContextInSpaceEnvironment() + } else { + const activeConn = this.activeConnection + if (activeConn && 'type' in activeConn && activeConn.type === 'iam') { + const state = this.auth.getStateMemento() + const smusConnections = (state.get('smus.connections') as any) || {} + const connectionMetadata = smusConnections[activeConn.id] + const isIamDomain = connectionMetadata?.isIamDomain || false + await setSmusIamModeContext(isIamDomain) + } else { + await setSmusIamModeContext(false) + } + } + })() + } + + /** + * Initializes IAM mode context in SMUS space environment + */ + private async initIamModeContextInSpaceEnvironment(): Promise { + try { + const resourceMetadata = getResourceMetadata() + if ( + resourceMetadata?.AdditionalMetadata?.DataZoneDomainId && + resourceMetadata?.AdditionalMetadata?.DataZoneDomainRegion + ) { + const domainId = resourceMetadata.AdditionalMetadata.DataZoneDomainId + const region = resourceMetadata.AdditionalMetadata.DataZoneDomainRegion + + const credentialsProvider = (await this.getDerCredentialsProvider()) as CredentialsProvider + + // Get DataZoneCustomClientHelper instance and fetch domain details to check if it's IAM mode + const datazoneCustomClientHelper = DataZoneCustomClientHelper.getInstance(credentialsProvider, region) + const domain = await datazoneCustomClientHelper.getDomain(domainId) + const isIamMode = isIamDomain({ + domainVersion: domain.domainVersion, + iamSignIns: domain.iamSignIns, + domainId: domainId, + }) + this.logger.debug(`Domain ${domainId} is in IAM mode: ${isIamMode}`) + await setSmusIamModeContext(isIamMode) + } + } catch (error) { + this.logger.error('Failed to check IAM mode in SMUS space environment: %s', error) + await setSmusIamModeContext(false) + } + } + + /** + * Stops SSH credential refresh for all projects + * Called when SMUS connection changes or extension deactivates + */ + public stopAllSshCredentialRefresh(): void { + this.logger.debug('Stopping SSH credential refresh for all projects') + for (const provider of this.projectCredentialProvidersCache.values()) { + provider.stopProactiveCredentialRefresh() + } + } + + /** + * Gets the active connection + */ + public get activeConnection(): SmusConnection | undefined { + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + const resourceMetadata = getResourceMetadata()! + if (resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion) { + // Return a mock connection object for SMUS space environment + // Include type property based on IAM mode context for telemetry + // Note: type will be undefined initially until mode is detected + const isIamMode = getContext('aws.smus.isIamMode') + return { + domainId: resourceMetadata.AdditionalMetadata!.DataZoneDomainId!, + ssoRegion: resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion!, + domainUrl: `https://${resourceMetadata.AdditionalMetadata!.DataZoneDomainId!}.sagemaker.${resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion!}.on.aws/`, + id: randomUUID(), + type: isIamMode !== undefined ? (isIamMode ? 'iam' : 'sso') : undefined, + } as any as SmusConnection + } else { + throw new ToolkitError('Domain region not found in metadata file.') + } + } + const baseConnection = this.secondaryAuth.activeConnection + + // If we have a connection, wrap it with SMUS metadata if available + if (baseConnection) { + const smusConnections = this.secondaryAuth.state.get('smus.connections') as any + const connectionMetadata = smusConnections?.[baseConnection.id] + + if (connectionMetadata) { + // For IAM connections, add the profile-specific metadata + if (baseConnection.type === 'iam') { + return { + ...baseConnection, + profileName: connectionMetadata.profileName, + region: connectionMetadata.region, + domainUrl: connectionMetadata.domainUrl, + domainId: connectionMetadata.domainId, + } as SmusIamConnection + } + // For SSO connections, the metadata is already in the connection object + // but we can ensure consistency by adding any missing properties + else if (baseConnection.type === 'sso') { + return { + ...baseConnection, + domainUrl: connectionMetadata.domainUrl || (baseConnection as any).domainUrl, + domainId: connectionMetadata.domainId || (baseConnection as any).domainId, + } as SmusConnection + } + } + } + + return baseConnection as SmusConnection | undefined + } + + /** + * Checks if using a saved connection + */ + public get isUsingSavedConnection() { + return this.secondaryAuth.hasSavedConnection + } + + /** + * Checks if the connection is valid + */ + public isConnectionValid(): boolean { + // When in SMUS space, the extension is already running in projet context and sign in is not needed + // Set isConnectionValid to always true + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + return true + } + return this.activeConnection !== undefined && !this.secondaryAuth.isConnectionExpired + } + + /** + * Checks if connected to SMUS + */ + public isConnected(): boolean { + // When in SMUS space, the extension is already running in projet context and sign in is not needed + // Set isConnected to always true + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + return true + } + return this.activeConnection !== undefined + } + + /** + * Restores the previous connection + * Validates domain metadata against profile and updates if needed before using saved connection + */ + public async restore() { + const logger = getLogger('smus') + + // Get the saved connection ID before restoring + const savedConnectionId = this.secondaryAuth.state.get('smus.savedConnectionId') as string + if (!savedConnectionId) { + logger.debug('No saved connection ID found, proceeding with normal restore') + await this.secondaryAuth.restoreConnection() + return + } + + // Get the saved connection metadata + const smusConnections = (this.secondaryAuth.state.get('smus.connections') as any) || {} + const connectionMetadata = smusConnections[savedConnectionId] + + // If no connection metadata exists, proceed with normal restore + if (!connectionMetadata) { + logger.debug('No connection metadata found, proceeding with normal restore') + await this.secondaryAuth.restoreConnection() + return + } + + const savedProfileName = connectionMetadata.profileName + + // If no profile name in metadata, proceed with normal restore + if (!savedProfileName) { + logger.debug('No profile name in metadata, proceeding with normal restore') + await this.secondaryAuth.restoreConnection() + return + } + + const profiles = await loadSharedCredentialsProfiles() + const profile = profiles[savedProfileName] + if (!profile) { + logger.debug(`No profile found with name: ${savedProfileName}`) + await this.secondaryAuth.restoreConnection() + return + } + const region = profile.region || 'not-set' + + const validation = await this.validateIamProfile(savedProfileName) + if (!validation.isValid) { + logger.debug(`Profile validation failed: ${validation.error}, proceeding with normal restore`) + await this.secondaryAuth.restoreConnection() + return + } + + let domainUrl + try { + logger.debug(`Finding IAM-based domain in region using profile ${savedProfileName}`) + + // Get DataZoneCustomClientHelper instance + const datazoneCustomClientHelper = DataZoneCustomClientHelper.getInstance( + await this.getCredentialsProviderForIamProfile(savedProfileName), + region + ) + + // Find the IAM-based domain using the client + const iamDomain = await datazoneCustomClientHelper.getIamDomain() + + if (!iamDomain) { + logger.warn(`No IAM-based domain found in region ${region}, proceeding with normal restore`) + await this.secondaryAuth.restoreConnection() + return + } + + logger.debug(`Found IAM-based domain: ${iamDomain.name} (${iamDomain.id})`) + + // Construct domain URL from the IAM-based domain + domainUrl = iamDomain.portalUrl || `https://${iamDomain.id}.sagemaker.${region}.on.aws/` + logger.debug(`Discovered IAM-based domain URL: ${domainUrl}`) + } catch (error) { + logger.error(`Failed to find IAM-based domain: ${error} , proceeding with normal restore`) + await this.secondaryAuth.restoreConnection() + return + } + + try { + logger.debug(`Validating domain metadata for saved connection ${savedConnectionId}`) + + if (!domainUrl) { + logger.info('No domain URL constructed, proceeding with normal restore') + await this.secondaryAuth.restoreConnection() + return + } + + const { domainId } = SmusUtils.extractDomainInfoFromUrl(domainUrl) + + // Compare with saved metadata + const savedDomainId = connectionMetadata.domainId + const savedRegion = connectionMetadata.region + + if (domainId === savedDomainId && region === savedRegion) { + logger.debug('Domain metadata matches, proceeding with normal restore') + } else { + logger.debug( + `SMUS: Domain metadata mismatch detected. Saved: ${savedDomainId}@${savedRegion}, Profile: ${domainId}@${region}. Updating metadata.` + ) + + // Update the metadata with API values + connectionMetadata.domainId = domainId + connectionMetadata.region = region + + // Save updated metadata + smusConnections[savedConnectionId] = connectionMetadata + await this.secondaryAuth.state.update('smus.connections', smusConnections) + + logger.debug('Successfully updated domain metadata') + } + } catch (error) { + logger.warn(`Failed to validate domain metadata: ${error}. Proceeding with normal restore.`) + } + + // Proceed with normal restore + await this.secondaryAuth.restoreConnection() + } + + /** + * Signs out from SMUS with different behavior based on connection type: + * - SSO connections: Deletes the connection (old behavior) + * - IAM connections: Forgets the connection without affecting the underlying IAM profile + */ + @withTelemetryContext({ name: 'signOut', class: authClassName }) + public async signOut() { + const logger = getLogger('smus') + + const activeConnection = this.activeConnection + if (!activeConnection) { + logger.debug('No active connection to sign out from') + return + } + + const connectionId = activeConnection.id + logger.info(`Signing out from connection ${connectionId}`) + + try { + // Clear SMUS-specific metadata from connections registry + const smusConnections = (this.secondaryAuth.state.get('smus.connections') as any) || {} + if (smusConnections[connectionId]) { + delete smusConnections[connectionId] + await this.secondaryAuth.state.update('smus.connections', smusConnections) + } + + // Handle sign-out based on connection type + // Check if this is a real connection (has 'type' property) vs mock connection in SMUS space + if ('type' in activeConnection && isSmusSsoConnection(activeConnection)) { + // For SSO connections, delete the connection (old behavior) + await this.secondaryAuth.deleteConnection() + logger.info(`Deleted SSO connection ${connectionId}`) + } else if ('type' in activeConnection) { + // For IAM connections, forget the connection without affecting the underlying IAM profile + await this.secondaryAuth.forgetConnection() + logger.info(`Forgot IAM connection ${connectionId} (preserved for other services)`) + + // Clear IAM mode context for IAM connections + await setSmusIamModeContext(false) + logger.debug('Cleared IAM mode context') + } else { + // Mock connection in SMUS space environment - no action needed + logger.info(`Sign out completed for mock connection ${connectionId}`) + } + + logger.info(`Successfully signed out from connection ${connectionId}`) + } catch (error) { + logger.error(`Failed to sign out from connection ${connectionId}:`, error) + throw new ToolkitError('Failed to sign out from SageMaker Unified Studio', { + code: SmusErrorCodes.SignOutFailed, + cause: error instanceof Error ? error : undefined, + }) + } + } + + /** + * Authenticates with SageMaker Unified Studio using SSO and a domain URL + * @param domainUrl The SageMaker Unified Studio domain URL + * @returns Promise resolving to the SSO connection + */ + @withTelemetryContext({ name: 'connectToSmusWithSso', class: authClassName }) + public async connectToSmusWithSso(domainUrl: string): Promise { + const logger = getLogger('smus') + + try { + // Extract domain info using SmusUtils + const { domainId, region } = SmusUtils.extractDomainInfoFromUrl(domainUrl) + + // Validate domain ID + if (!domainId) { + throw new ToolkitError('Invalid domain URL format', { code: SmusErrorCodes.InvalidDomainUrl }) + } + + logger.info(`Connecting to domain ${domainId} in region ${region}`) + + // Check if we already have a connection for this domain + const existingConn = (await this.auth.listConnections()).find( + (c): c is SmusConnection => + isValidSmusConnection(c) && (c as any).domainUrl?.toLowerCase() === domainUrl.toLowerCase() + ) + + if (existingConn) { + const connectionState = this.auth.getConnectionState(existingConn) + logger.info(`Found existing connection ${existingConn.id} with state: ${connectionState}`) + + // If connection is valid, use it directly without triggering new auth flow + if (connectionState === 'valid') { + logger.info('Using existing valid connection') + + // Only SSO connections can be used with connectToSmusWithSso + if (isSmusSsoConnection(existingConn)) { + // Use the existing SSO connection + const result = await this.secondaryAuth.useNewConnection(existingConn) + + // Auto-invoke project selection after successful sign-in (but not in SMUS space environment) + if (!SmusUtils.isInSmusSpaceEnvironment()) { + void vscode.commands.executeCommand('aws.smus.switchProject') + } + + return result as SmusConnection + } + } + + // If connection is invalid or expired, handle based on connection type + if (connectionState === 'invalid') { + // Only SSO connections can be reauthenticated + if (isSmusSsoConnection(existingConn)) { + logger.info('Existing SSO connection is invalid, reauthenticating') + const reauthenticatedConn = await this.reauthenticate(existingConn) + + // Create the SMUS connection wrapper + const smusConn: SmusConnection = { + ...reauthenticatedConn, + domainUrl, + domainId, + } + + const result = await this.secondaryAuth.useNewConnection(smusConn) + logger.debug(`Reauthenticated connection successfully, id=${result.id}`) + + // Auto-invoke project selection after successful reauthentication (but not in SMUS space environment) + if (!SmusUtils.isInSmusSpaceEnvironment()) { + void vscode.commands.executeCommand('aws.smus.switchProject') + } + + return result as SmusConnection + } + } + } + + // No existing connection found, create a new one + logger.info('No existing connection found, creating new connection') + + // Get SSO instance info from DataZone + const ssoInstanceInfo = await SmusUtils.getSsoInstanceInfo(domainUrl) + + // Create a new connection with appropriate scope based on domain URL + const profile = createSmusProfile(domainUrl, domainId, ssoInstanceInfo.issuerUrl, ssoInstanceInfo.region) + const newConn = await this.auth.createConnection(profile) + logger.debug(`Created new connection ${newConn.id}`) + + const smusConn: SmusConnection = { + ...newConn, + domainUrl, + domainId, + } + + const result = await this.secondaryAuth.useNewConnection(smusConn) + + // Auto-invoke project selection after successful sign-in (but not in SMUS space environment) + if (!SmusUtils.isInSmusSpaceEnvironment()) { + void vscode.commands.executeCommand('aws.smus.switchProject') + } + + return result as SmusConnection + } catch (e) { + throw ToolkitError.chain(e, 'Failed to connect to SageMaker Unified Studio', { + code: SmusErrorCodes.FailedToConnect, + }) + } + } + + /** + * Authenticates with SageMaker Unified Studio using IAM credential profile + * @param profileName The AWS credential profile name + * @param region The AWS region + * @param domainUrl The SageMaker Unified Studio domain URL + * @param isIamDomain Whether the domain is an IAM-based domain + * @returns Promise resolving to the IAM connection + */ + @withTelemetryContext({ name: 'connectWithIamProfile', class: authClassName }) + public async connectWithIamProfile( + profileName: string, + region: string, + domainUrl: string, + isIamDomain: boolean = false + ): Promise { + const logger = getLogger('smus') + + try { + // Extract domain info using SmusUtils + const { domainId } = SmusUtils.extractDomainInfoFromUrl(domainUrl) + + // Validate domain ID + if (!domainId) { + throw new ToolkitError('Invalid domain URL format', { code: SmusErrorCodes.InvalidDomainUrl }) + } + + logger.info(`Connecting with IAM profile ${profileName} to domain ${domainId} in region ${region}`) + + // Note: Credential validation is already done in the orchestrator via validateIamProfile() + // No need for redundant validation here + + // Check if we already have a basic IAM connection for this profile + const profileId = `profile:${profileName}` + const existingConn = await this.auth.getConnection({ id: profileId }) + + if (existingConn && existingConn.type === 'iam') { + logger.info(`Found existing IAM profile connection ${profileId}`) + + // Store SMUS metadata in the connections registry + const smusConnections = (this.secondaryAuth.state.get('smus.connections') as any) || {} + smusConnections[existingConn.id] = { + profileName, + region, + domainUrl, + domainId, + isIamDomain, + } + await this.secondaryAuth.state.update('smus.connections', smusConnections) + + // Use the basic IAM connection with secondaryAuth + await this.secondaryAuth.useNewConnection(existingConn) + + // Ensure the connection state is validated + await this.auth.refreshConnectionState(existingConn) + logger.debug( + `SMUS: Using existing IAM connection as SMUS connection successfully, id=${existingConn.id}` + ) + + // Set IAM mode context if this is an IAM-based domain + if (isIamDomain) { + await setSmusIamModeContext(true) + logger.debug('Set IAM mode context to true') + } + + // Return a SMUS IAM connection wrapper for the caller + const smusIamConn: SmusIamConnection = { + ...existingConn, + profileName, + region, + domainUrl, + domainId, + } + + return smusIamConn + } + + // If no existing connection, the auth system should have created one during profile validation + // This shouldn't happen if credentials are valid, but let's handle it gracefully + throw new ToolkitError( + `IAM profile connection not found for '${profileName}'. Please check your AWS credentials configuration.`, + { + code: SmusErrorCodes.ConnectionNotFound, + } + ) + } catch (e) { + throw ToolkitError.chain(e, 'Failed to connect to SageMaker Unified Studio with IAM profile', { + code: SmusErrorCodes.FailedToConnect, + }) + } + } + + /** + * Validates an IAM credential profile using the existing Toolkit validation infrastructure + * @param profileName Profile name to validate + * @returns Promise resolving to validation result + */ + public async validateIamProfile(profileName: string): Promise<{ isValid: boolean; error?: string }> { + const logger = getLogger('smus') + + try { + logger.debug(`Validating IAM profile: ${profileName}`) + + // Create credentials ID for the profile using the existing Toolkit pattern + const credentialsId: CredentialsId = { + credentialSource: SharedCredentialsProvider.getProviderType(), + credentialTypeId: profileName, + } + + // Get the provider using the existing manager + const provider = await CredentialsProviderManager.getInstance().getCredentialsProvider(credentialsId) + if (!provider) { + return { + isValid: false, + error: `Profile '${profileName}' not found or not available`, + } + } + + // Get credentials and validate using the existing Toolkit validation logic + // This includes proper telemetry and error handling + const credentials = await provider.getCredentials() + await globals.loginManager.validateCredentials( + credentials, + provider.getEndpointUrl?.(), + provider.getDefaultRegion() // Use the region from the profile, not hardcoded + ) + + logger.debug(`Profile validation successful: ${profileName}`) + return { isValid: true } + } catch (error) { + logger.error(`Profile validation failed: ${profileName}`, error) + return { + isValid: false, + error: `Invalid profile '${profileName}' - ${(error as Error).message}`, + } + } + } + + /** + * Gets credentials for an IAM profile using Toolkit providers + * @param profileName AWS profile name + * @returns Promise resolving to credentials + */ + public async getCredentialsForIamProfile(profileName: string): Promise { + const logger = getLogger('smus') + + try { + logger.debug(`Getting credentials for IAM profile: ${profileName}`) + + // Create credentials ID for the profile using the existing Toolkit pattern + const credentialsId: CredentialsId = { + credentialSource: SharedCredentialsProvider.getProviderType(), + credentialTypeId: profileName, + } + + // Get the provider using the existing manager + const provider = await CredentialsProviderManager.getInstance().getCredentialsProvider(credentialsId) + if (!provider) { + throw new ToolkitError(`Profile '${profileName}' not found or not available`, { + code: SmusErrorCodes.ProfileNotFound, + }) + } + + // Get credentials using the existing Toolkit provider + const credentials = await provider.getCredentials() + + logger.debug(`Successfully retrieved credentials for IAM profile: ${profileName}`) + return credentials + } catch (error) { + logger.error(`Failed to get credentials for IAM profile ${profileName}: %s`, error) + throw new ToolkitError( + `Failed to get credentials for profile '${profileName}': ${(error as Error).message}`, + { + code: SmusErrorCodes.CredentialRetrievalFailed, + cause: error instanceof Error ? error : undefined, + } + ) + } + } + + /** + * Gets the underlying credentials provider for an IAM profile + * @param profileName AWS profile name + * @returns Promise resolving to the credentials provider + */ + public async getCredentialsProviderForIamProfile(profileName: string): Promise { + const logger = getLogger('smus') + logger.debug(`Getting credentials provider for IAM profile: ${profileName}`) + + // Create credentials ID for the profile using the existing Toolkit pattern + const credentialsId: CredentialsId = { + credentialSource: SharedCredentialsProvider.getProviderType(), + credentialTypeId: profileName, + } + + // Get the provider using the existing manager + const provider = await CredentialsProviderManager.getInstance().getCredentialsProvider(credentialsId) + if (!provider) { + throw new ToolkitError(`Profile '${profileName}' not found or not available`, { + code: SmusErrorCodes.ProfileNotFound, + }) + } + + // Return the underlying provider directly + // This allows callers to use the provider's full interface including caching and refresh + return provider + } + + /** + * Gets the cached caller identity ARN for the active IAM connection + * Fetches from STS if not cached or if connection has changed + * Only works for IAM connections - returns undefined for SSO connections + * @returns Promise resolving to the ARN, or undefined if not available or not an IAM connection + */ + public async getCachedIamCallerIdentityArn(): Promise { + const logger = getLogger('smus') + try { + const activeConn = this.activeConnection + // Only cache for IAM connections + if (!activeConn || activeConn.type !== 'iam') { + return undefined + } + + // Check if we have a cached ARN for this connection + if (this.iamCallerIdentityCache && this.iamCallerIdentityCache.connectionId === activeConn.id) { + logger.debug('Using cached IAM caller identity ARN') + return this.iamCallerIdentityCache.arn + } + + // Fetch fresh caller identity + logger.debug('Fetching IAM caller identity from STS') + const smusConnections = (this.secondaryAuth.state.get('smus.connections') as any) || {} + const connectionMetadata = smusConnections[activeConn.id] + + if (!connectionMetadata?.profileName || !connectionMetadata?.region) { + logger.debug('Missing profile name or region in connection metadata') + return undefined + } + + const credentials = await this.getCredentialsForIamProfile(connectionMetadata.profileName) + const stsClient = new DefaultStsClient(connectionMetadata.region, credentials) + const callerIdentity = await stsClient.getCallerIdentity() + + if (!callerIdentity.Arn) { + logger.debug('No ARN found in caller identity') + return undefined + } + + // Cache the result + this.iamCallerIdentityCache = { + arn: callerIdentity.Arn, + connectionId: activeConn.id, + } + logger.debug(`Cached IAM caller identity ARN for connection ${activeConn.id}`) + + return callerIdentity.Arn + } catch (error) { + logger.warn(`Failed to get IAM caller identity: %s`, error) + return undefined + } + } + + /** + * Gets the session name from the cached IAM caller identity + * Only works for IAM connections - returns undefined for SSO connections + * @returns Promise resolving to the session name, or undefined if not available or not an IAM connection + */ + public async getSessionName(): Promise { + const arn = await this.getCachedIamCallerIdentityArn() + if (!arn) { + return undefined + } + + const sessionName = SmusUtils.extractSessionNameFromArn(arn) + this.logger.debug(`Extracted session name: ${sessionName || 'none'}`) + return sessionName + } + + /** + * Gets the role ARN from the cached IAM caller identity + * Converts assumed role ARN to IAM role ARN format + * Only works for IAM connections - returns undefined for SSO connections + * @returns Promise resolving to the IAM role ARN, or undefined if not available or not an IAM connection + */ + public async getIamPrincipalArn(): Promise { + const arn = await this.getCachedIamCallerIdentityArn() + if (!arn) { + return undefined + } + + // Convert assumed role ARN to IAM role ARN + const roleArn = SmusUtils.convertAssumedRoleArnToIamRoleArn(arn) + this.logger.debug(`Extracted role ARN: ${roleArn || 'none'}`) + return roleArn + } + + /** + * Clears the cached IAM caller identity + * Should be called when connection changes or credentials are refreshed + */ + private clearIamCallerIdentityCache(): void { + this.iamCallerIdentityCache = undefined + this.logger.debug('Cleared IAM caller identity cache') + } + + /** + * Reauthenticates an existing connection + * @param conn Connection to reauthenticate + * @returns Promise resolving to the reauthenticated connection + */ + @withTelemetryContext({ name: 'reauthenticate', class: authClassName }) + public async reauthenticate(conn: SmusConnection): Promise { + try { + // Check if this is an IAM connection + if (isSmusIamConnection(conn)) { + // For IAM connections, show options menu + this.logger.debug('Showing IAM credential expiry options for reauthentication') + const result = await showIamCredentialExpiryOptions(this, conn, globals.context) + + // Handle the result - for most actions, return the original connection + // The actions have already been performed (sign out, edit credentials, etc.) + if (result.action === IamCredentialExpiryAction.SignOut) { + throw new ToolkitError('User signed out from connection', { cancelled: true }) + } else if (result.action === IamCredentialExpiryAction.Cancelled) { + throw new ToolkitError('Reauthentication cancelled by user', { cancelled: true }) + } + + // For Reauthenticate, EditCredentials, and SwitchProfile, return the connection + return conn + } else { + // For SSO connections, use existing re-auth flow + const reauthenticatedConn = await this.auth.reauthenticate(conn) + + // Re-add SMUS-specific properties that aren't preserved by the base auth system + return { + ...reauthenticatedConn, + domainUrl: conn.domainUrl, + domainId: conn.domainId, + } as SmusConnection + } + } catch (err) { + throw ToolkitError.chain(err, 'Unable to reauthenticate SageMaker Unified Studio connection.') + } + } + + /** + * Shows a reauthentication prompt to the user + * @param conn Connection to reauthenticate + */ + public async showReauthenticationPrompt(conn: SmusConnection): Promise { + await showReauthenticateMessage({ + message: localizedText.connectionExpired('SageMaker Unified Studio'), + connect: localizedText.reauthenticate, + suppressId: 'smusConnectionExpired', + settings: ToolkitPromptSettings.instance, + source: 'SageMaker Unified Studio', + reauthFunc: async () => { + await this.reauthenticate(conn) + }, + }) + } + + /** + * Gets the current SSO access token for the active connection + * @returns Promise resolving to the access token string + * @throws ToolkitError if unable to retrieve access token + */ + public async getAccessToken(): Promise { + const logger = getLogger('smus') + + const connection = this.activeConnection + if (!connection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + // Only SSO connections have access tokens + if (!isSmusSsoConnection(connection)) { + throw new ToolkitError('Access tokens are only available for SSO connections', { + code: SmusErrorCodes.InvalidConnectionType, + }) + } + + try { + // Type assertion is safe here because we've already checked with isSmusSsoConnection + const accessToken = await this.auth.getSsoAccessToken(connection as SsoConnection) + logger.debug(`Successfully retrieved SSO access token for connection ${connection.id}`) + + return accessToken + } catch (err) { + logger.error(`Failed to retrieve SSO access token for connection ${connection.id}: %s`, err) + + // Check if this is a reauth error that should be handled by showing SMUS-specific prompt + if (err instanceof ToolkitError && err.code === 'InvalidConnection') { + // Re-throw the error to maintain the error flow + logger.debug( + `SMUS: Auth connection has been marked invalid - Likely due to expiry. Reauthentication flow will be triggered, ignoring error` + ) + } + + throw new ToolkitError(`Failed to retrieve SSO access token for connection ${connection.id}`, { + code: SmusErrorCodes.RedeemAccessTokenFailed, + cause: err instanceof Error ? err : undefined, + }) + } + } + + /** + * Gets or creates a project credentials provider for the specified project + * @param projectId The project ID to get credentials for + * @returns Promise resolving to the project credentials provider + */ + public async getProjectCredentialProvider(projectId: string): Promise { + const logger = getLogger('smus') + + if (!this.activeConnection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + logger.debug(`Getting project provider for project ${projectId}`) + + // Check if we already have a cached provider for this project + if (this.projectCredentialProvidersCache.has(projectId)) { + logger.debug('Using cached project provider') + return this.projectCredentialProvidersCache.get(projectId)! + } + + logger.debug('Creating new project provider') + // Create a new project provider and cache it + const projectProvider = new ProjectRoleCredentialsProvider(this, projectId) + this.projectCredentialProvidersCache.set(projectId, projectProvider) + + logger.debug('Cached new project provider') + + return projectProvider + } + + /** + * Gets or creates a connection credentials provider for the specified connection + * @param connectionId The connection ID to get credentials for + * @param projectId The project ID that owns the connection + * @param region The region for the connection + * @returns Promise resolving to the connection credentials provider + */ + public async getConnectionCredentialsProvider( + connectionId: string, + projectId: string, + region: string + ): Promise { + const logger = getLogger('smus') + + if (!this.activeConnection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + const cacheKey = `${this.getDomainId()}:${projectId}:${connectionId}` + logger.debug(`Getting connection provider for connection ${connectionId}`) + + // Check if we already have a cached provider for this connection + if (this.connectionCredentialProvidersCache.has(cacheKey)) { + logger.debug('Using cached connection provider') + return this.connectionCredentialProvidersCache.get(cacheKey)! + } + + logger.debug('Creating new connection provider') + // Create a new connection provider and cache it + const connectionProvider = new ConnectionCredentialsProvider(this, connectionId, projectId) + this.connectionCredentialProvidersCache.set(cacheKey, connectionProvider) + + logger.debug('Cached new connection provider') + + return connectionProvider + } + + /** + * Gets the domain ID from the active connection + * @returns Domain ID + */ + public getDomainId(): string { + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + return getResourceMetadata()!.AdditionalMetadata!.DataZoneDomainId! + } + + if (!this.activeConnection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + // For SMUS connections (both SSO and IAM) with domainId property + if ('domainId' in this.activeConnection) { + return (this.activeConnection as any).domainId + } + + throw new ToolkitError('Domain ID not available. Please reconnect to SMUS.', { + code: SmusErrorCodes.NoActiveConnection, + }) + } + + /** + * Gets the domain URL from the active connection + * @returns Domain URL + */ + public getDomainUrl(): string { + if (!this.activeConnection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + // For SMUS connections (both SSO and IAM) with domainUrl property + if ('domainUrl' in this.activeConnection) { + return (this.activeConnection as any).domainUrl + } + + throw new ToolkitError('Domain URL not available. Please reconnect to SMUS.', { + code: SmusErrorCodes.NoActiveConnection, + }) + } + + /** + * Gets the AWS account ID for the active domain connection + * In SMUS space environment, extracts from ResourceArn in metadata + * Otherwise, makes an STS GetCallerIdentity call using DER credentials and caches the result + * @returns Promise resolving to the domain's AWS account ID + * @throws ToolkitError if unable to retrieve account ID + */ + public async getDomainAccountId(): Promise { + const logger = getLogger('smus') + + // Return cached value if available + if (this.cachedDomainAccountId) { + logger.debug('Using cached domain account ID') + return this.cachedDomainAccountId + } + + // If in SMUS space environment, extract account ID from resource-metadata file + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + const accountId = await extractAccountIdFromResourceMetadata() + + // Cache the account ID + this.cachedDomainAccountId = accountId + logger.debug(`Successfully cached domain account ID: ${accountId}`) + + return accountId + } + + if (!this.activeConnection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + // Use existing STS GetCallerIdentity implementation for non-SMUS space environments + try { + logger.debug('Fetching domain account ID via STS GetCallerIdentity') + + let credentialsProvider + if (getContext('aws.smus.isIamMode')) { + credentialsProvider = await this.getCredentialsProviderForIamProfile( + (this.activeConnection as SmusIamConnection).profileName + ) + } else { + credentialsProvider = await this.getDerCredentialsProvider() + } + // Get the region for STS client + const region = this.getDomainRegion() + + // Create STS client with DER credentials + const stsClient = new DefaultStsClient(region, await credentialsProvider.getCredentials()) + + // Make GetCallerIdentity call + const callerIdentity = await stsClient.getCallerIdentity() + + if (!callerIdentity.Account) { + throw new ToolkitError('Account ID not found in STS GetCallerIdentity response', { + code: SmusErrorCodes.AccountIdNotFound, + }) + } + + // Cache the account ID + this.cachedDomainAccountId = callerIdentity.Account + + logger.debug(`Successfully retrieved and cached domain account ID: ${callerIdentity.Account}`) + + return callerIdentity.Account + } catch (err) { + logger.error(`Failed to retrieve domain account ID: %s`, err) + + throw new ToolkitError('Failed to retrieve AWS account ID for active domain connection', { + code: SmusErrorCodes.GetDomainAccountIdFailed, + cause: err instanceof Error ? err : undefined, + }) + } + } + + /** + * Gets the AWS account ID for a specific project using project credentials + * In SMUS space environment, extracts from ResourceArn in metadata (same as domain account) + * Otherwise, makes an STS GetCallerIdentity call using project credentials + * @param projectId The DataZone project ID + * @returns Promise resolving to the project's AWS account ID + */ + public async getProjectAccountId(projectId: string): Promise { + const logger = getLogger('smus') + + // Return cached value if available + if (this.cachedProjectAccountIds.has(projectId)) { + logger.debug(`Using cached project account ID for project ${projectId}`) + return this.cachedProjectAccountIds.get(projectId)! + } + + // If in SMUS space environment, extract account ID from resource-metadata file + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + const accountId = await extractAccountIdFromResourceMetadata() + + // Cache the account ID + this.cachedProjectAccountIds.set(projectId, accountId) + logger.debug(`Successfully cached project account ID for project ${projectId}: ${accountId}`) + + return accountId + } + + if (!this.activeConnection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + // For non-SMUS space environments, use project credentials with STS + try { + logger.debug('Fetching project account ID via STS GetCallerIdentity with project credentials') + + // Get project credentials + const projectCredProvider = await this.getProjectCredentialProvider(projectId) + const projectCreds = await projectCredProvider.getCredentials() + + // Get project region from tooling environment + const dzClient = await createDZClientBaseOnDomainMode(this) + const toolingEnv = await dzClient.getToolingEnvironment(projectId) + const projectRegion = toolingEnv.awsAccountRegion + + if (!projectRegion) { + throw new ToolkitError('No AWS account region found in tooling environment', { + code: SmusErrorCodes.RegionNotFound, + }) + } + + // Use STS to get account ID from project credentials + const stsClient = new DefaultStsClient(projectRegion, projectCreds) + const callerIdentity = await stsClient.getCallerIdentity() + + if (!callerIdentity.Account) { + throw new ToolkitError('Account ID not found in STS GetCallerIdentity response', { + code: SmusErrorCodes.AccountIdNotFound, + }) + } + + // Cache the account ID + this.cachedProjectAccountIds.set(projectId, callerIdentity.Account) + logger.debug( + `Successfully retrieved and cached project account ID for project ${projectId}: ${callerIdentity.Account}` + ) + + return callerIdentity.Account + } catch (err) { + logger.error('Failed to get project account ID: %s', err as Error) + throw new ToolkitError(`Failed to get project account ID: ${(err as Error).message}`, { + code: SmusErrorCodes.GetProjectAccountIdFailed, + }) + } + } + + public getDomainRegion(): string { + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + const resourceMetadata = getResourceMetadata()! + if (resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion) { + return resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion + } else { + throw new ToolkitError('Domain region not found in metadata file.') + } + } + + const connection = this.activeConnection + if (!connection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + // Handle different connection types + if (isSmusSsoConnection(connection)) { + return connection.ssoRegion + } + + // For SMUS connections (both SSO and IAM) with region property + if ('region' in connection) { + return (connection as any).region + } + + throw new ToolkitError('Domain region not available. Please reconnect to SMUS.', { + code: SmusErrorCodes.NoActiveConnection, + }) + } + + /** + * Gets or creates a cached credentials provider for the active connection + * @returns Promise resolving to the credentials provider + */ + public async getDerCredentialsProvider(): Promise { + const logger = getLogger('smus') + + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + // When in SMUS space, DomainExecutionRoleCreds can be found in config file + // Read the credentials from credential profile DomainExecutionRoleCreds + try { + // Load AWS config file to check profile configuration + const { configFile } = await loadSharedConfigFiles() + const profileConfig = configFile['DomainExecutionRoleCreds'] + + if (profileConfig?.credential_process) { + // Normal SMUS domain: Use the profile with credential_process + logger.debug('Using DomainExecutionRoleCreds profile with credential_process') + const credentials = fromIni({ profile: 'DomainExecutionRoleCreds' }) + return convertToToolkitCredentialProvider( + async () => await credentials(), + 'DomainExecutionRoleCreds', + `smus-der-profile:${this.getDomainId()}:${this.getDomainRegion()}`, + this.getDomainRegion() + ) + } else if (profileConfig?.credential_source === 'EcsContainer') { + // IAM-based domain with EcsContainer: Use ECS container credentials directly + // The environment has AWS_CONTAINER_CREDENTIALS_RELATIVE_URI set, so use fromContainerMetadata + // which properly handles the ECS credential endpoint + logger.debug('IAM-based domain detected, using ECS container credentials') + const credentials = fromContainerMetadata({ + timeout: 5000, + maxRetries: 3, + }) + return convertToToolkitCredentialProvider( + async () => await credentials(), + 'EcsContainer', + `smus-ecs-container:${this.getDomainId()}:${this.getDomainRegion()}`, + this.getDomainRegion() + ) + } else { + // Fallback: try the profile anyway + logger.debug( + 'SMUS: Unknown profile configuration, attempting to use DomainExecutionRoleCreds profile' + ) + const credentials = fromIni({ profile: 'DomainExecutionRoleCreds' }) + return convertToToolkitCredentialProvider( + async () => await credentials(), + 'DomainExecutionRoleCreds-fallback', + `smus-der-fallback:${this.getDomainId()}:${this.getDomainRegion()}`, + this.getDomainRegion() + ) + } + } catch (error) { + logger.error('Failed to load config file, falling back to default credential chain: %s', error) + const credentials = fromNodeProviderChain() + return convertToToolkitCredentialProvider( + async () => await credentials(), + 'NodeProviderChain', + `smus-node-provider-chain:${this.getDomainId()}:${this.getDomainRegion()}`, + this.getDomainRegion() + ) + } + } + + const connection = this.activeConnection + if (!connection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + // Domain Execution Role credentials are only available for SSO connections + if (!isSmusSsoConnection(connection)) { + throw new ToolkitError('Domain Execution Role credentials are only available for SSO connections', { + code: SmusErrorCodes.InvalidConnectionType, + }) + } + + // Create a cache key based on the connection details + const cacheKey = `${connection.ssoRegion}:${connection.domainId}` + + logger.debug(`Getting credentials provider for cache key: ${cacheKey}`) + + // Check if we already have a cached provider + if (this.credentialsProviderCache.has(cacheKey)) { + logger.debug('Using cached credentials provider') + return this.credentialsProviderCache.get(cacheKey) + } + + logger.debug('Creating new credentials provider') + + // Create a new provider and cache it + const provider = new DomainExecRoleCredentialsProvider( + connection.domainUrl, + connection.domainId, + connection.ssoRegion, + async () => await this.getAccessToken() + ) + + this.credentialsProviderCache.set(cacheKey, provider) + logger.debug('Cached new credentials provider') + + return provider + } + + /** + * Invalidates all cached credentials (for all connections) + * Used during connection changes or logout + */ + private async invalidateAllCredentialsInCache(): Promise { + const logger = getLogger('smus') + logger.debug('Invalidating all cached credentials') + + // Clear all cached DER providers and their internal credentials + for (const [cacheKey, provider] of this.credentialsProviderCache.entries()) { + try { + provider.invalidate() // This will clear the provider's internal cache + logger.debug(`Invalidated credentials for cache key: ${cacheKey}`) + } catch (err) { + logger.warn(`Failed to invalidate credentials for cache key ${cacheKey}: %s`, err) + } + } + + // Clear all cached project providers and their internal credentials + + await this.invalidateAllProjectCredentialsInCache() + // Clear all cached connection providers and their internal credentials + for (const [cacheKey, connectionProvider] of this.connectionCredentialProvidersCache.entries()) { + try { + connectionProvider.invalidate() // This will clear the connection provider's internal cache + logger.debug(`Invalidated connection credentials for cache key: ${cacheKey}`) + } catch (err) { + logger.warn(`Failed to invalidate connection credentials for cache key ${cacheKey}: %s`, err) + } + } + + // Clear cached domain account ID + this.cachedDomainAccountId = undefined + logger.debug('Cleared cached domain account ID') + + // Clear cached project account IDs + this.cachedProjectAccountIds.clear() + logger.debug('Cleared cached project account IDs') + } + + /** + * Invalidates all project cached credentials + */ + public async invalidateAllProjectCredentialsInCache(): Promise { + const logger = getLogger('smus') + logger.debug('Invalidating all cached project credentials') + + for (const [projectId, projectProvider] of this.projectCredentialProvidersCache.entries()) { + try { + projectProvider.invalidate() // This will clear the project provider's internal cache + logger.debug(`Invalidated project credentials for project: ${projectId}`) + } catch (err) { + logger.warn(`Failed to invalidate project credentials for project ${projectId}: %s`, err) + } + } + } + + /** + * Stops SSH credential refresh and cleans up resources + */ + public dispose(): void { + this.logger.debug('Disposing authentication provider and all cached providers') + + // Dispose all project providers + for (const provider of this.projectCredentialProvidersCache.values()) { + provider.dispose() + } + this.projectCredentialProvidersCache.clear() + + // Dispose all connection providers + for (const provider of this.connectionCredentialProvidersCache.values()) { + provider.dispose() + } + this.connectionCredentialProvidersCache.clear() + + // Dispose all DER providers in the general cache + for (const provider of this.credentialsProviderCache.values()) { + if (provider && typeof provider.dispose === 'function') { + provider.dispose() + } + } + this.credentialsProviderCache.clear() + + // Clear cached domain account ID + this.cachedDomainAccountId = undefined + + // Clear cached project account IDs + this.cachedProjectAccountIds.clear() + + // Clear cached IAM caller identity + this.clearIamCallerIdentityCache() + + DataZoneClient.dispose() + DataZoneCustomClientHelper.dispose() + + this.logger.debug('Successfully disposed authentication provider') + } + + static #instance: SmusAuthenticationProvider | undefined + + public static get instance(): SmusAuthenticationProvider | undefined { + return SmusAuthenticationProvider.#instance + } + + public static fromContext() { + return (this.#instance ??= new this()) + } + + public async invalidateConnection(): Promise { + // When in SMUS space, the extension is already running in projet context and sign in is not needed + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + return + } + + if (!this.activeConnection) { + return + } + + // For IAM connections, actively validate credentials + // No action needed for SSO as the connection is automatically updated + if (isSmusIamConnection(this.activeConnection)) { + try { + const validation = await this.validateIamProfile(this.activeConnection.profileName) + await this.auth.updateConnectionState( + this.activeConnection.id, + validation.isValid ? 'valid' : 'invalid' + ) + } catch { + await this.auth.updateConnectionState(this.activeConnection.id, 'invalid') + } + } + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/ui/authenticationMethodSelection.ts b/packages/core/src/sagemakerunifiedstudio/auth/ui/authenticationMethodSelection.ts new file mode 100644 index 00000000000..e209a15b1ca --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/ui/authenticationMethodSelection.ts @@ -0,0 +1,111 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../../shared/logger/logger' +import { ToolkitError } from '../../../shared/errors' +import { SmusErrorCodes } from '../../shared/smusUtils' + +/** + * Authentication method types supported by SMUS + */ +export type SmusAuthenticationMethod = 'sso' | 'iam' + +/** + * Result of authentication method selection + */ +export interface AuthenticationMethodSelection { + method: SmusAuthenticationMethod +} + +/** + * Authentication method selection dialog for SMUS + */ +export class SmusAuthenticationMethodSelector { + private static readonly logger = getLogger('smus') + + /** + * Shows the authentication method selection dialog matching the Figma design + * @param defaultMethod Optional default method to pre-select + * @returns Promise resolving to the selected authentication method + */ + public static async showAuthenticationMethodSelection( + defaultMethod?: SmusAuthenticationMethod + ): Promise { + const logger = this.logger + + const iamOption: vscode.QuickPickItem = { + label: '$(key) IAM Credential', + detail: 'Use IAM credentials to access resources in SageMaker Unified Studio IAM-based domains.', + } + + const ssoOption: vscode.QuickPickItem = { + label: '$(organization) IAM Identity Center', + detail: 'Use Identity Center to access resources in SageMaker Unified Studio IdC-based domains.', + } + + const options = [iamOption, ssoOption] + + // Set default selection based on preference + let defaultIndex = 0 + if (defaultMethod === 'sso') { + defaultIndex = 1 + } + + const quickPick = vscode.window.createQuickPick() + quickPick.title = 'Select a sign in method' + quickPick.placeholder = 'Choose how you want to authenticate with SageMaker Unified Studio' + quickPick.items = options + quickPick.canSelectMany = false + quickPick.ignoreFocusOut = true + + // Pre-select the default method + if (options[defaultIndex]) { + quickPick.activeItems = [options[defaultIndex]] + } + + return new Promise((resolve, reject) => { + let isCompleted = false + + quickPick.onDidAccept(() => { + const selectedItem = quickPick.selectedItems[0] + if (!selectedItem) { + quickPick.dispose() + reject( + new ToolkitError('No authentication method selected', { + code: SmusErrorCodes.UserCancelled, + cancelled: true, + }) + ) + return + } + + const method: SmusAuthenticationMethod = selectedItem === iamOption ? 'iam' : 'sso' + + logger.debug(`User selected authentication method: ${method}`) + + isCompleted = true + quickPick.dispose() + + // Return the selected method without asking about preferences + resolve({ method }) + }) + + quickPick.onDidHide(() => { + if (!isCompleted) { + quickPick.dispose() + reject( + new ToolkitError('Authentication method selection cancelled', { + code: SmusErrorCodes.UserCancelled, + cancelled: true, + }) + ) + } + }) + + quickPick.show() + }) + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/ui/iamProfileSelection.ts b/packages/core/src/sagemakerunifiedstudio/auth/ui/iamProfileSelection.ts new file mode 100644 index 00000000000..fd61fc70f50 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/ui/iamProfileSelection.ts @@ -0,0 +1,1311 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as path from 'path' +import { getLogger } from '../../../shared/logger/logger' +import { ToolkitError } from '../../../shared/errors' +import { loadSharedCredentialsProfiles } from '../../../auth/credentials/sharedCredentials' +import { getCredentialsFilename, getConfigFilename } from '../../../auth/credentials/sharedCredentialsFile' +import { SmusErrorCodes, DataZoneServiceId } from '../../shared/smusUtils' +import globals from '../../../shared/extensionGlobals' +import fs from '../../../shared/fs/fs' + +/** + * Actions available in the credential management dialog + */ +enum CredentialManagementAction { + EditCredentialsFile = 'EDIT_CREDENTIALS_FILE', + EditConfigFile = 'EDIT_CONFIG_FILE', + AddNewProfile = 'ADD_NEW_PROFILE', +} + +/** + * Actions available in the profile selection dialog + */ +enum ProfileSelectionAction { + SelectProfile = 'SELECT_PROFILE', + ManageCredentials = 'MANAGE_CREDENTIALS', +} + +/** + * Actions available in the session token input dialog + */ +enum SessionTokenAction { + Skip = 'SKIP', + UseToken = 'USE_TOKEN', + Warning = 'WARNING', +} + +/** + * Result of IAM profile selection + */ +export interface IamProfileSelection { + profileName: string + region: string +} + +/** + * Result indicating user chose to edit credential files + */ +export interface IamProfileEditingInProgress { + isEditing: true + message: string +} + +/** + * Result indicating user chose to go back + */ +export interface IamProfileBackNavigation { + isBack: true + message: string +} + +/** + * IAM profile selection interface for SMUS + */ +export class SmusIamProfileSelector { + private static readonly logger = getLogger('smus') + + // Validation regex patterns (based on AWS STS API specifications) + // Reference: https://docs.aws.amazon.com/STS/latest/APIReference/API_Credentials.html + private static readonly profileNamePattern = /^[a-zA-Z0-9_-]+$/ + // AWS AccessKeyId: 16-128 chars, pattern [\w]* (alphanumeric + underscore) + private static readonly accessKeyIdPattern = /^[a-zA-Z0-9_]*$/ + // AWS SecretAccessKey and SessionToken: Required per STS API, but no pattern/length constraints specified + private static readonly regionLinePattern = /^region\s*=.*$/m + + /** + * Creates a QuickPick with common settings for input dialogs + * @param title Title for the QuickPick + * @param placeholder Placeholder text + * @returns Configured QuickPick instance + */ + private static createInputQuickPick(title: string, placeholder: string): vscode.QuickPick { + const quickPick = vscode.window.createQuickPick() + quickPick.title = title + quickPick.placeholder = placeholder + quickPick.canSelectMany = false + quickPick.ignoreFocusOut = true + quickPick.buttons = [vscode.QuickInputButtons.Back] + return quickPick + } + + /** + * Shows the IAM profile selection dialog matching the Figma design + * @returns Promise resolving to the selected profile and region, editing status, or back navigation + */ + public static async showIamProfileSelection(): Promise< + IamProfileSelection | IamProfileEditingInProgress | IamProfileBackNavigation + > { + const logger = this.logger + + try { + // Load available credential profiles + const profiles = await loadSharedCredentialsProfiles() + const profileNames = Object.keys(profiles) + + // Create QuickPick items for profiles + const profileItems: (vscode.QuickPickItem & { + action: ProfileSelectionAction + profileName: string + region: string + })[] = profileNames.map((profileName) => { + const profile = profiles[profileName] + const region = profile.region || 'not-set' + + return { + label: `$(key) ${profileName}`, + description: `IAM Credentials, configured locally (${region})`, + detail: `Profile: ${profileName} | Region: ${region}`, + action: ProfileSelectionAction.SelectProfile, + profileName, + region, + } + }) + + // Add "Add or edit credentials" option + const addCredentialsItem: vscode.QuickPickItem & { action: ProfileSelectionAction } = { + label: '$(add) Add or edit credentials', + description: 'Manage AWS credential profiles', + detail: 'Add new profiles or edit existing credential files', + action: ProfileSelectionAction.ManageCredentials, + } + + const options = [...profileItems, addCredentialsItem] + + const quickPick = vscode.window.createQuickPick() + quickPick.title = 'Select an IAM Profile' + quickPick.placeholder = 'Choose an AWS credential profile to authenticate with SageMaker Unified Studio' + quickPick.items = options + quickPick.canSelectMany = false + quickPick.ignoreFocusOut = true + + // Add back button + const backButton = vscode.QuickInputButtons.Back + quickPick.buttons = [backButton] + + return new Promise((resolve, reject) => { + let isCompleted = false + + quickPick.onDidAccept(() => { + const selectedItem = quickPick.selectedItems[0] + if (!selectedItem) { + quickPick.dispose() + reject( + new ToolkitError('No profile selected', { + code: SmusErrorCodes.UserCancelled, + cancelled: true, + }) + ) + return + } + + isCompleted = true + quickPick.dispose() + + const itemWithAction = selectedItem as vscode.QuickPickItem & { + action: ProfileSelectionAction + profileName?: string + region?: string + } + + // Check if user selected "Add or edit credentials" + if (itemWithAction.action === ProfileSelectionAction.ManageCredentials) { + // Handle the async credential management flow + void (async () => { + try { + const managementResult = await SmusIamProfileSelector.showCredentialManagement() + + // Check if a new profile was created (returns IamProfileSelection) + if (typeof managementResult === 'object' && 'profileName' in managementResult) { + // User created a new profile, use it directly + logger.debug( + `SMUS Auth: Using newly created profile: ${managementResult.profileName}` + ) + resolve(managementResult) + } else if (managementResult === true) { + // User wants to restart profile selection (e.g., clicked back) + const result = await SmusIamProfileSelector.showIamProfileSelection() + resolve(result) + } else { + // User chose to edit files, return a special result indicating this + resolve({ + isEditing: true, + message: + 'User chose to edit credential files. Please complete setup and try again.', + }) + } + } catch (error) { + // Handle user cancellation gracefully + if (error instanceof ToolkitError && error.code === SmusErrorCodes.UserCancelled) { + resolve({ + isEditing: true, + message: 'User cancelled credential management.', + }) + } else { + reject(error) + } + } + })() + return + } + + // User selected an existing profile + // Ensure we have profile data (should always be present for SelectProfile action) + if (!itemWithAction.profileName || !itemWithAction.region) { + reject(new ToolkitError('Invalid profile selection', { code: 'InvalidProfileSelection' })) + return + } + + const profileName = itemWithAction.profileName + const profileRegion = itemWithAction.region + + logger.debug(`User selected profile: ${profileName}`) + + // Check if region is not set and prompt for region selection + if (profileRegion === 'not-set') { + void (async () => { + try { + const selectedRegion = await SmusIamProfileSelector.showRegionSelection() + + // Check if user clicked back on region selection + if (selectedRegion === 'BACK') { + resolve({ + isBack: true, + message: 'User chose to go back from region selection.', + }) + return + } + + // Update the profile with the selected region + await SmusIamProfileSelector.updateProfileRegion(profileName, selectedRegion) + + resolve({ + profileName: profileName, + region: selectedRegion, + }) + } catch (error) { + reject(error) + } + })() + } else { + resolve({ + profileName: profileName, + region: profileRegion, + }) + } + }) + + quickPick.onDidTriggerButton((button) => { + if (button === vscode.QuickInputButtons.Back) { + isCompleted = true + quickPick.dispose() + resolve({ + isBack: true, + message: 'User chose to go back to authentication method selection.', + }) + } + }) + + quickPick.onDidHide(() => { + if (!isCompleted) { + quickPick.dispose() + reject( + new ToolkitError('Profile selection cancelled', { + code: SmusErrorCodes.UserCancelled, + cancelled: true, + }) + ) + } + }) + + quickPick.show() + }) + } catch (error) { + // Don't log or chain user cancellation as an error + if (error instanceof ToolkitError && error.code === SmusErrorCodes.UserCancelled) { + throw error + } + logger.error('Failed to show IAM profile selection: %s', error) + throw ToolkitError.chain(error, 'Failed to show IAM profile selection') + } + } + + /** + * Shows region selection dialog for IAM authentication + * @param options Configuration options for the region selection dialog + * @returns Promise resolving to the selected region or 'BACK' if user wants to go back + */ + public static async showRegionSelection(options?: { + defaultRegion?: string + title?: string + placeholder?: string + returnBackOnCancel?: boolean + }): Promise { + const logger = this.logger + + // Get regions where DataZone service is available + const allRegions = globals.regionProvider.getRegions() + const dataZoneRegions = allRegions.filter((region) => + globals.regionProvider.isServiceInRegion(DataZoneServiceId, region.id) + ) + + // If no regions found with DataZone service, fall back to all regions + const regions = dataZoneRegions.length > 0 ? dataZoneRegions : allRegions + + const regionItems: vscode.QuickPickItem[] = regions.map( + (region) => + ({ + label: region.name, + description: region.id, + detail: `AWS Region: ${region.id}`, + regionCode: region.id, + }) as vscode.QuickPickItem & { regionCode: string } + ) + + const quickPick = this.createInputQuickPick( + options?.title ?? 'Select AWS Region', + options?.placeholder ?? 'Choose the AWS region for SageMaker Unified Studio' + ) + quickPick.items = regionItems + + // Allow users to find matches by typing in the region code (e.g., us-east-1) + quickPick.matchOnDescription = true + + // Pre-select default region if provided + if (options?.defaultRegion) { + const defaultItem = regionItems.find((item) => (item as any).regionCode === options.defaultRegion) + if (defaultItem) { + quickPick.activeItems = [defaultItem] + } + } + + return new Promise((resolve, reject) => { + let isCompleted = false + + quickPick.onDidAccept(() => { + const selectedItem = quickPick.selectedItems[0] + if (!selectedItem) { + if (options?.returnBackOnCancel) { + quickPick.dispose() + resolve('BACK') + } else { + quickPick.dispose() + reject( + new ToolkitError('No region selected', { + code: SmusErrorCodes.UserCancelled, + cancelled: true, + }) + ) + } + return + } + + isCompleted = true + quickPick.dispose() + + const regionItem = selectedItem as vscode.QuickPickItem & { regionCode: string } + + logger.debug(`User selected region: ${regionItem.regionCode}`) + + resolve(regionItem.regionCode) + }) + + quickPick.onDidTriggerButton((button) => { + if (button === vscode.QuickInputButtons.Back) { + isCompleted = true + quickPick.dispose() + resolve('BACK') + } + }) + + quickPick.onDidHide(() => { + if (!isCompleted) { + quickPick.dispose() + if (options?.returnBackOnCancel) { + resolve('BACK') + } else { + reject( + new ToolkitError('Region selection cancelled', { + code: SmusErrorCodes.UserCancelled, + cancelled: true, + }) + ) + } + } + }) + + quickPick.show() + }) + } + + /** + * Shows credential management options (Add/Edit credentials) + * @returns Promise resolving to boolean indicating if profile selection should restart, or profile data if a new profile was created + */ + public static async showCredentialManagement(): Promise { + const logger = this.logger + + logger.debug('Showing credential management options') + + const options: (vscode.QuickPickItem & { action: CredentialManagementAction })[] = [ + { + label: '$(file-text) Edit AWS Credentials File', + description: 'Open ~/.aws/credentials file for editing', + detail: 'Edit existing credential profiles or add new ones', + action: CredentialManagementAction.EditCredentialsFile, + }, + { + label: '$(file-text) Edit AWS Config File', + description: 'Open ~/.aws/config file for editing', + detail: 'Edit AWS configuration settings and profiles', + action: CredentialManagementAction.EditConfigFile, + }, + { + label: '$(add) Add New Profile', + description: 'Create a new AWS credential profile', + detail: 'Interactive setup for a new credential profile', + action: CredentialManagementAction.AddNewProfile, + }, + ] + + const quickPick = vscode.window.createQuickPick() + quickPick.title = 'Manage AWS Credentials' + quickPick.placeholder = 'Choose how you want to manage your AWS credentials' + quickPick.items = options + quickPick.canSelectMany = false + quickPick.ignoreFocusOut = true + + // Add back button + const backButton = vscode.QuickInputButtons.Back + quickPick.buttons = [backButton] + + return new Promise((resolve, reject) => { + let isCompleted = false + + quickPick.onDidAccept(() => { + const selectedItem = quickPick.selectedItems[0] + if (!selectedItem) { + quickPick.dispose() + reject( + new ToolkitError('No option selected', { code: SmusErrorCodes.UserCancelled, cancelled: true }) + ) + return + } + + isCompleted = true + quickPick.dispose() + + // Handle the async operations after disposing the quick pick + void (async () => { + try { + const itemWithAction = selectedItem as vscode.QuickPickItem & { + action: CredentialManagementAction + } + + switch (itemWithAction.action) { + case CredentialManagementAction.EditCredentialsFile: { + const result = await this.openAwsFile('credentials') + // If user clicked "Select Profile", restart profile selection + resolve(result === 'RESTART_PROFILE_SELECTION') + break + } + case CredentialManagementAction.EditConfigFile: { + const result = await this.openAwsFile('config') + // If user clicked "Select Profile", restart profile selection + resolve(result === 'RESTART_PROFILE_SELECTION') + break + } + case CredentialManagementAction.AddNewProfile: { + const newProfile = await this.addNewProfile() + // Return the newly created profile data to use it directly + resolve(newProfile) + break + } + } + } catch (error) { + if (error instanceof ToolkitError && error.code === SmusErrorCodes.UserCancelled) { + // User cancelled, don't treat as error + reject(error) + } else { + reject(error) + } + } + })() + }) + + quickPick.onDidTriggerButton((button) => { + if (button === vscode.QuickInputButtons.Back) { + isCompleted = true + quickPick.dispose() + // User wants to go back to profile selection + resolve(true) + } + }) + + quickPick.onDidHide(() => { + if (!isCompleted) { + quickPick.dispose() + reject( + new ToolkitError('Credential management cancelled', { + code: SmusErrorCodes.UserCancelled, + cancelled: true, + }) + ) + } + }) + + quickPick.show() + }) + } + + /** + * Opens the AWS credentials file in VS Code editor + */ + /** + * Opens an AWS configuration file in VS Code editor + * @param fileType Type of file to open ('credentials' or 'config') + */ + private static async openAwsFile(fileType: 'credentials' | 'config'): Promise { + const logger = this.logger + const isCredentials = fileType === 'credentials' + + try { + const filePath = isCredentials ? getCredentialsFilename() : getConfigFilename() + const fileLabel = isCredentials ? 'credentials' : 'config' + + logger.debug(`Opening ${fileLabel} file: ${filePath}`) + + // Ensure the .aws directory exists + await this.ensureAwsDirectoryExists() + + // Create the file if it doesn't exist + if (!(await fs.existsFile(filePath))) { + await fs.writeFile(filePath, '') + logger.debug(`Created new ${fileLabel} file`) + } + + // Open the file in VS Code + const document = await vscode.workspace.openTextDocument(filePath) + await vscode.window.showTextDocument(document) + + logger.debug(`${fileLabel} file opened successfully`) + } catch (error) { + const fileLabel = isCredentials ? 'credentials' : 'config' + logger.error(`Failed to open ${fileLabel} file: %s`, error) + throw new ToolkitError(`Failed to open AWS ${fileLabel} file: ${(error as Error).message}`, { + code: isCredentials ? 'CredentialsFileError' : 'ConfigFileError', + }) + } + } + + /** + * Interactive flow to add a new AWS credential profile with back navigation + * @returns Promise resolving to the newly created profile data + */ + private static async addNewProfile(): Promise { + const logger = this.logger + + try { + logger.debug('Starting add new profile flow') + + const profileData = await this.collectProfileData() + + if (profileData === 'BACK') { + // User navigated back, throw error to go back to credential management + throw new ToolkitError('User navigated back', { code: SmusErrorCodes.UserCancelled, cancelled: true }) + } + + // Add the profile to credentials file + await this.addProfileToCredentialsFile( + profileData.profileName, + profileData.accessKeyId, + profileData.secretAccessKey, + profileData.sessionToken, + profileData.region + ) + + // Show success message + void vscode.window.showInformationMessage( + `AWS profile '${profileData.profileName}' has been added successfully and will be used for authentication.` + ) + + logger.debug(`Successfully added new profile: ${profileData.profileName}`) + + // Return the profile data to use it directly + return { + profileName: profileData.profileName, + region: profileData.region, + } + } catch (error) { + // Only log actual errors, not user cancellations + if (error instanceof ToolkitError && error.code === SmusErrorCodes.UserCancelled) { + logger.debug('User cancelled add new profile flow') + throw error // Re-throw for telemetry but don't log as error + } + logger.error('Failed to add new profile: %s', error) + throw new ToolkitError(`Failed to add new profile: ${(error as Error).message}`, { + code: 'AddProfileError', + }) + } + } + + /** + * Collects profile data through a multi-step flow with back navigation + */ + private static async collectProfileData(): Promise< + | { + profileName: string + accessKeyId: string + secretAccessKey: string + sessionToken?: string + region: string + } + | 'BACK' + > { + let currentStep = 1 + let profileName = '' + let accessKeyId = '' + let secretAccessKey = '' + let sessionToken = '' + let region = '' + + while (currentStep <= 5) { + switch (currentStep) { + case 1: { + // Step 1: Profile Name + const result = await this.getProfileNameInput() + if (result === 'BACK') { + return 'BACK' // User wants to go back - exit to credential management menu + } + profileName = result + currentStep = 2 + break + } + case 2: { + // Step 2: Access Key ID + const result = await this.getAccessKeyIdInput() + if (result === 'BACK') { + currentStep = 1 // Go back to step 1 + } else { + accessKeyId = result + currentStep = 3 + } + break + } + case 3: { + // Step 3: Secret Access Key + const result = await this.getSecretAccessKeyInput() + if (result === 'BACK') { + currentStep = 2 // Go back to step 2 + } else { + secretAccessKey = result + currentStep = 4 + } + break + } + case 4: { + // Step 4: Session Token (optional) + const result = await this.getSessionTokenInput() + if (result === 'BACK') { + currentStep = 3 // Go back to step 3 + } else { + sessionToken = result + currentStep = 5 + } + break + } + case 5: { + // Step 5: Region + const result = await this.showRegionSelection({ + title: 'Add New AWS Profile - Step 5 of 5', + placeholder: 'Select a default region', + returnBackOnCancel: true, + }) + if (result === 'BACK') { + currentStep = 4 // Go back to step 4 + } else { + region = result + currentStep = 6 // Exit the loop + } + break + } + } + } + + return { + profileName, + accessKeyId, + secretAccessKey, + sessionToken: sessionToken || undefined, + region, // Region is always set since step 5 is required + } + } + + /** + * Gets profile name input with back navigation and existing profile validation + */ + private static async getProfileNameInput(): Promise { + return new Promise((resolve) => { + const quickPick = this.createInputQuickPick( + 'Add New AWS Profile - Step 1 of 5', + 'Type a profile name (e.g., my-profile, dev, prod)' + ) + quickPick.items = [] + + let isCompleted = false + + quickPick.onDidTriggerButton((button) => { + if (button === vscode.QuickInputButtons.Back) { + isCompleted = true + quickPick.dispose() + resolve('BACK') + } + }) + + quickPick.onDidChangeValue(async (value) => { + // Show placeholder when empty + if (!value) { + quickPick.items = [ + { + label: '$(edit) Enter profile name', + description: 'e.g., my-profile, dev, prod', + detail: 'Profile names can contain letters, numbers, hyphens, and underscores', + }, + ] + return + } + + // Validate input as user types + if (value.includes(' ')) { + quickPick.items = [ + { + label: `${value}`, + description: '$(error) Cannot contain spaces', + detail: 'Valid characters: letters, numbers, hyphens, underscores', + }, + ] + } else if (!this.profileNamePattern.test(value)) { + quickPick.items = [ + { + label: `${value}`, + description: '$(error) Invalid characters', + detail: 'Profile names can only contain letters, numbers, hyphens, and underscores', + }, + ] + } else if (value.length < 2) { + quickPick.items = [ + { + label: `${value}`, + description: `$(info) Too short (${value.length}/2 min)`, + detail: 'Profile names should be at least 2 characters long', + }, + ] + } else { + // Check if profile already exists + try { + const profiles = await loadSharedCredentialsProfiles() + const profileExists = profiles[value] !== undefined + + if (profileExists) { + quickPick.items = [ + { + label: `${value}`, + description: '$(warning) Profile exists - will be overwritten', + detail: 'Press Enter to overwrite the existing profile', + }, + ] + } else { + quickPick.items = [ + { + label: `${value}`, + description: `$(check) Valid (${value.length} characters)`, + detail: 'Press Enter to use this profile name', + }, + ] + } + } catch (error) { + // If we can't load profiles, just show as valid + quickPick.items = [ + { + label: `${value}`, + description: `$(check) Valid (${value.length} characters)`, + detail: 'Press Enter to use this profile name', + }, + ] + } + } + }) + + quickPick.onDidAccept(async () => { + const value = quickPick.value.trim() + + // Validate final input + if (!value || value.length < 2) { + return // Don't accept empty or too short input + } + if (value.includes(' ')) { + return // Don't accept names with spaces + } + if (!this.profileNamePattern.test(value)) { + return // Don't accept invalid characters + } + + // Check if profile exists and ask for confirmation + try { + const profiles = await loadSharedCredentialsProfiles() + const profileExists = profiles[value] !== undefined + + if (profileExists) { + isCompleted = true + quickPick.dispose() + + // Ask for confirmation to overwrite + const overwrite = await vscode.window.showWarningMessage( + `Profile '${value}' already exists. Do you want to overwrite it?`, + { modal: true }, + 'Overwrite' + ) + + if (overwrite === 'Overwrite') { + resolve(value) + } else { + // User cancelled, restart the input + const result = await this.getProfileNameInput() + resolve(result) + } + return + } + } catch (error) { + // If we can't load profiles, just continue + } + + isCompleted = true + quickPick.dispose() + resolve(value) + }) + + quickPick.onDidHide(() => { + if (!isCompleted) { + quickPick.dispose() + resolve('BACK') + } + }) + + quickPick.show() + }) + } + + /** + * Gets access key ID input with back navigation + */ + private static async getAccessKeyIdInput(): Promise { + return new Promise((resolve) => { + const quickPick = this.createInputQuickPick( + 'Add New AWS Profile - Step 2 of 5', + 'Type your AWS Access Key ID (e.g., AKIAIOSFODNN7EXAMPLE)' + ) + quickPick.items = [] + + let isCompleted = false + + quickPick.onDidTriggerButton((button) => { + if (button === vscode.QuickInputButtons.Back) { + isCompleted = true + quickPick.dispose() + resolve('BACK') + } + }) + + quickPick.onDidChangeValue((value) => { + // Show placeholder when empty + if (!value) { + quickPick.items = [ + { + label: '$(key) Enter AWS Access Key ID', + description: 'e.g., AKIAIOSFODNN7EXAMPLE', + detail: 'Access Key IDs are typically 16-32 characters long', + }, + ] + return + } + + // Validate input as user types (AWS STS API: 16-128 chars, pattern [\w]*) + // Reference: https://docs.aws.amazon.com/STS/latest/APIReference/API_Credentials.html + if (!this.accessKeyIdPattern.test(value)) { + quickPick.items = [ + { + label: `${value}`, + description: '$(error) Invalid characters', + detail: 'Access Key IDs can only contain letters, numbers, and underscores', + }, + ] + } else if (value.length < 16) { + quickPick.items = [ + { + label: `${value}`, + description: `$(info) Too short (${value.length}/16 min)`, + detail: 'AWS Access Key IDs must be 16-128 characters long', + }, + ] + } else if (value.length > 128) { + quickPick.items = [ + { + label: `${value}`, + description: `$(error) Too long (${value.length}/128 max)`, + detail: 'AWS Access Key IDs must be 16-128 characters long', + }, + ] + } else { + quickPick.items = [ + { + label: `${value}`, + description: `$(check) Valid (${value.length} characters)`, + detail: 'Press Enter to use this Access Key ID', + }, + ] + } + }) + + quickPick.onDidAccept(() => { + const value = quickPick.value.trim() + + // Validate final input (AWS STS API: 16-128 chars, pattern [\w]*) + // Reference: https://docs.aws.amazon.com/STS/latest/APIReference/API_Credentials.html + if (!value) { + return // Don't accept empty input + } + if (!this.accessKeyIdPattern.test(value)) { + return // Don't accept invalid characters + } + if (value.length < 16 || value.length > 128) { + return // Don't accept invalid length + } + + isCompleted = true + quickPick.dispose() + resolve(value) + }) + + quickPick.onDidHide(() => { + if (!isCompleted) { + quickPick.dispose() + resolve('BACK') + } + }) + + quickPick.show() + }) + } + + /** + * Gets secret access key input with back navigation + */ + private static async getSecretAccessKeyInput(): Promise { + return new Promise((resolve) => { + const quickPick = this.createInputQuickPick( + 'Add New AWS Profile - Step 3 of 5', + 'Type your AWS Secret Access Key (will be hidden when typing)' + ) + quickPick.items = [] + + let isCompleted = false + + quickPick.onDidTriggerButton((button) => { + if (button === vscode.QuickInputButtons.Back) { + isCompleted = true + quickPick.dispose() + resolve('BACK') + } + }) + + quickPick.onDidChangeValue((value) => { + // Show placeholder when empty + if (!value) { + quickPick.items = [ + { + label: '$(lock) Enter AWS Secret Access Key', + description: 'Required field', + detail: 'Enter your AWS Secret Access Key', + }, + ] + return + } + + // AWS STS API: Required, no specific pattern/length constraints in docs + // Reference: https://docs.aws.amazon.com/STS/latest/APIReference/API_Credentials.html + quickPick.items = [ + { + label: '•'.repeat(Math.min(value.length, 40)), + description: `$(check) ${value.length} characters entered`, + detail: 'Press Enter to continue', + }, + ] + }) + + quickPick.onDidAccept(() => { + const value = quickPick.value.trim() + + // Validate final input - AWS STS API only requires non-empty + // Reference: https://docs.aws.amazon.com/STS/latest/APIReference/API_Credentials.html + if (!value) { + return // Don't accept empty input + } + + isCompleted = true + quickPick.dispose() + resolve(value) + }) + + quickPick.onDidHide(() => { + if (!isCompleted) { + quickPick.dispose() + resolve('BACK') + } + }) + + quickPick.show() + }) + } + + /** + * Gets session token input with back navigation + */ + private static async getSessionTokenInput(): Promise { + return new Promise((resolve) => { + const quickPick = this.createInputQuickPick( + 'Add New AWS Profile - Step 4 of 5', + 'Enter your AWS Session Token (optional for temporary credentials)' + ) + + // Start with skip option only + quickPick.items = [ + { + label: '$(arrow-right) Skip', + description: 'Skip session token (for permanent credentials)', + detail: 'Use this for regular IAM user access keys', + action: SessionTokenAction.Skip, + } as vscode.QuickPickItem & { action: SessionTokenAction }, + ] + + let isCompleted = false + + quickPick.onDidTriggerButton((button) => { + if (button === vscode.QuickInputButtons.Back) { + isCompleted = true + quickPick.dispose() + resolve('BACK') + } + }) + + quickPick.onDidChangeValue((value) => { + if (!value) { + // Show skip option when empty + quickPick.items = [ + { + label: '$(arrow-right) Skip', + description: 'Skip session token (for permanent credentials)', + detail: 'Use this for regular IAM user access keys', + action: SessionTokenAction.Skip, + } as vscode.QuickPickItem & { action: SessionTokenAction }, + ] + return + } + + // AWS STS API: Required for temporary credentials, no specific pattern/length constraints in docs + // Reference: https://docs.aws.amazon.com/STS/latest/APIReference/API_Credentials.html + quickPick.items = [ + { + label: '•'.repeat(Math.min(value.length, 40)), + description: `$(check) ${value.length} characters entered`, + detail: 'Press Enter to use this session token', + action: SessionTokenAction.UseToken, + } as vscode.QuickPickItem & { action: SessionTokenAction }, + { + label: '$(arrow-right) Skip', + description: 'Skip session token (for permanent credentials)', + detail: 'Use this for regular IAM user access keys', + action: SessionTokenAction.Skip, + } as vscode.QuickPickItem & { action: SessionTokenAction }, + ] + }) + + quickPick.onDidAccept(() => { + const selectedItem = quickPick.selectedItems[0] + const currentValue = quickPick.value + + isCompleted = true + quickPick.dispose() + + // If user typed something and pressed Enter without selecting an item, use the typed value (trimmed) + if (!selectedItem && currentValue) { + resolve(currentValue.trim()) + return + } + + // If no selection with empty value, skip + if (!selectedItem) { + resolve('') + return + } + + const itemWithAction = selectedItem as vscode.QuickPickItem & { action: SessionTokenAction } + + // Handle based on action + switch (itemWithAction.action) { + case SessionTokenAction.Skip: + resolve('') + break + case SessionTokenAction.UseToken: + resolve(currentValue.trim()) + break + case SessionTokenAction.Warning: + // User can still proceed with warning, use the typed value + resolve(currentValue.trim()) + break + default: + resolve('') + } + }) + + quickPick.onDidHide(() => { + if (!isCompleted) { + quickPick.dispose() + resolve('BACK') + } + }) + + quickPick.show() + }) + } + + /** + * Ensures the ~/.aws directory exists + */ + private static async ensureAwsDirectoryExists(): Promise { + const awsDir = path.join(fs.getUserHomeDir(), '.aws') + if (!(await fs.existsDir(awsDir))) { + await fs.mkdir(awsDir) + } + } + + /** + * Adds a new profile to the credentials file or overwrites existing one + */ + private static async addProfileToCredentialsFile( + profileName: string, + accessKeyId: string, + secretAccessKey: string, + sessionToken?: string, + region?: string + ): Promise { + const credentialsPath = getCredentialsFilename() + + // Ensure the .aws directory exists + await this.ensureAwsDirectoryExists() + + // Read existing content or create new + let content = '' + if (await fs.existsFile(credentialsPath)) { + content = await fs.readFileText(credentialsPath) + } + + // Create new profile lines (no spaces around =) + const newProfileLines = [ + `[${profileName}]`, + `aws_access_key_id=${accessKeyId}`, + `aws_secret_access_key=${secretAccessKey}`, + ] + + if (sessionToken) { + newProfileLines.push(`aws_session_token=${sessionToken}`) + } + + if (region) { + newProfileLines.push(`region=${region}`) + } + + // Parse the file line by line to handle profile replacement properly + const lines = content.split('\n') + const newLines: string[] = [] + let inTargetProfile = false + let profileFound = false + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim() + + // Check if this is a profile header + if (line.startsWith('[') && line.endsWith(']')) { + const currentProfileName = line.slice(1, -1) + + if (currentProfileName === profileName) { + // Found the target profile - replace it + if (!profileFound) { + newLines.push(...newProfileLines) + profileFound = true + } + inTargetProfile = true + continue + } else { + // Different profile - end replacement mode + inTargetProfile = false + newLines.push(lines[i]) + } + } else if (!inTargetProfile) { + // Not in target profile, keep the line + newLines.push(lines[i]) + } + // If inTargetProfile is true, we skip the line (removing old profile content) + } + + // If profile wasn't found, add it at the end + if (!profileFound) { + if (newLines.length > 0 && newLines[newLines.length - 1].trim() !== '') { + newLines.push('') // Add blank line before new profile + } + newLines.push(...newProfileLines) + } + + // Update content with the new lines + content = newLines.join('\n') + + // Write back to file + await fs.writeFile(credentialsPath, content) + } + + /** + * Updates an existing profile with a new region + */ + private static async updateProfileRegion(profileName: string, region: string): Promise { + const logger = this.logger + + try { + logger.debug(`Updating profile ${profileName} with region ${region}`) + + const credentialsPath = getCredentialsFilename() + + if (!(await fs.existsFile(credentialsPath))) { + throw new ToolkitError('Credentials file not found', { code: 'CredentialsFileNotFound' }) + } + + // Read the current credentials file + const content = await fs.readFileText(credentialsPath) + + // Find the profile section + const profileSectionRegex = new RegExp(`^\\[${profileName}\\]$`, 'm') + const profileMatch = content.match(profileSectionRegex) + + if (!profileMatch) { + throw new ToolkitError(`Profile ${profileName} not found in credentials file`, { + code: 'ProfileNotFound', + }) + } + + // Find the next profile section or end of file + const profileStartIndex = profileMatch.index! + const nextProfileMatch = content.slice(profileStartIndex + 1).match(/^\[.*\]$/m) + const profileEndIndex = nextProfileMatch ? profileStartIndex + 1 + nextProfileMatch.index! : content.length + + // Extract the profile section + const profileSection = content.slice(profileStartIndex, profileEndIndex) + + // Check if region already exists in the profile + let updatedProfileSection: string + + if (this.regionLinePattern.test(profileSection)) { + // Replace existing region + updatedProfileSection = profileSection.replace(this.regionLinePattern, `region = ${region}`) + } else { + // Add region to the profile (before any empty lines at the end) + const lines = profileSection.split('\n') + // Find the last non-empty line index (compatible with older JS versions) + let lastNonEmptyIndex = -1 + for (let i = lines.length - 1; i >= 0; i--) { + if (lines[i].trim() !== '') { + lastNonEmptyIndex = i + break + } + } + lines.splice(lastNonEmptyIndex + 1, 0, `region = ${region}`) + updatedProfileSection = lines.join('\n') + } + + // Replace the profile section in the content + const updatedContent = + content.slice(0, profileStartIndex) + updatedProfileSection + content.slice(profileEndIndex) + + // Write back to file + await fs.writeFile(credentialsPath, updatedContent) + + logger.debug(`Successfully updated profile ${profileName} with region ${region}`) + } catch (error) { + logger.error('Failed to update profile region: %s', error) + throw new ToolkitError(`Failed to update profile region: ${(error as Error).message}`, { + code: 'UpdateProfileError', + }) + } + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/ui/ssoAuthentication.ts b/packages/core/src/sagemakerunifiedstudio/auth/ui/ssoAuthentication.ts new file mode 100644 index 00000000000..2d2efaa15f8 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/ui/ssoAuthentication.ts @@ -0,0 +1,108 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { SmusUtils } from '../../shared/smusUtils' + +/** + * SSO authentication UI components for SMUS + */ +export class SmusSsoAuthenticationUI { + /** + * Shows domain URL input with back button support + */ + public static async showDomainUrlInput(): Promise { + return new Promise((resolve) => { + const quickPick = vscode.window.createQuickPick() + quickPick.title = 'SageMaker Unified Studio Authentication' + quickPick.placeholder = 'Enter your SageMaker Unified Studio Domain URL' + quickPick.canSelectMany = false + quickPick.ignoreFocusOut = true + + // Add back button + const backButton = vscode.QuickInputButtons.Back + quickPick.buttons = [backButton] + + // Start with placeholder item + quickPick.items = [ + { + label: '$(globe) Enter Domain URL', + description: 'e.g., https://dzd_xxxxxxxxx.sagemaker.region.on.aws', + detail: 'Type your SageMaker Unified Studio domain URL above', + }, + ] + + let isCompleted = false + + quickPick.onDidTriggerButton((button) => { + if (button === backButton) { + isCompleted = true + quickPick.dispose() + resolve('BACK') + } + }) + + quickPick.onDidChangeValue((value) => { + if (!value) { + quickPick.items = [ + { + label: '$(globe) Enter Domain URL', + description: 'e.g., https://dzd_xxxxxxxxx.sagemaker.region.on.aws', + detail: 'Type your SageMaker Unified Studio domain URL above', + }, + ] + return + } + + // Validate input as user types + const validation = SmusUtils.validateDomainUrl(value) + if (validation) { + quickPick.items = [ + { + label: '$(error) Invalid Domain URL', + description: validation, + detail: `Current input: "${value}"`, + }, + ] + } else { + quickPick.items = [ + { + label: '$(check) Use this Domain URL', + description: 'Press Enter to connect', + detail: `Domain URL: ${value}`, + }, + ] + } + }) + + quickPick.onDidAccept(() => { + const value = quickPick.value.trim() + + // Validate final input + if (!value) { + return // Don't accept empty input + } + + const validation = SmusUtils.validateDomainUrl(value) + if (validation) { + return // Don't accept invalid URLs + } + + isCompleted = true + quickPick.dispose() + resolve(value) + }) + + quickPick.onDidHide(() => { + if (!isCompleted) { + quickPick.dispose() + resolve(undefined) // User cancelled + } + }) + + quickPick.show() + }) + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/activation.ts b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/activation.ts new file mode 100644 index 00000000000..97ffadacc69 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/activation.ts @@ -0,0 +1,80 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { Constants } from './models/constants' +import { + getStatusBarProviders, + showConnectionQuickPick, + showProjectQuickPick, + parseNotebookCells, +} from './commands/commands' + +/** + * Activates the SageMaker Unified Studio Connection Magics Selector feature. + * + * @param extensionContext The extension context + */ +export async function activate(extensionContext: vscode.ExtensionContext): Promise { + extensionContext.subscriptions.push( + vscode.commands.registerCommand(Constants.CONNECTION_COMMAND, () => showConnectionQuickPick()), + vscode.commands.registerCommand(Constants.PROJECT_COMMAND, () => showProjectQuickPick()) + ) + + if ('NotebookEdit' in vscode) { + const { connectionProvider, projectProvider, separatorProvider } = getStatusBarProviders() + + extensionContext.subscriptions.push( + vscode.notebooks.registerNotebookCellStatusBarItemProvider('jupyter-notebook', connectionProvider), + vscode.notebooks.registerNotebookCellStatusBarItemProvider('jupyter-notebook', projectProvider), + vscode.notebooks.registerNotebookCellStatusBarItemProvider('jupyter-notebook', separatorProvider) + ) + + extensionContext.subscriptions.push( + vscode.window.onDidChangeActiveNotebookEditor(async () => { + await parseNotebookCells() + }) + ) + + extensionContext.subscriptions.push(vscode.workspace.onDidChangeTextDocument(handleTextDocumentChange)) + + void parseNotebookCells() + } +} + +/** + * Handles text document changes to update status bar when cells are manually edited + */ +function handleTextDocumentChange(event: vscode.TextDocumentChangeEvent): void { + if (event.document.uri.scheme !== 'vscode-notebook-cell') { + return + } + + const editor = vscode.window.activeNotebookEditor + if (!editor) { + return + } + + let changedCell: vscode.NotebookCell | undefined + for (let i = 0; i < editor.notebook.cellCount; i++) { + const cell = editor.notebook.cellAt(i) + if (cell.document.uri.toString() === event.document.uri.toString()) { + changedCell = cell + break + } + } + + if (changedCell && changedCell.kind === vscode.NotebookCellKind.Code) { + const { notebookStateManager } = require('./services/notebookStateManager') + + notebookStateManager.parseCellMagic(changedCell) + + setTimeout(() => { + const { connectionProvider, projectProvider } = getStatusBarProviders() + connectionProvider.refreshCellStatusBar() + projectProvider.refreshCellStatusBar() + }, 100) + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/client/connectedSpaceDataZoneClient.ts b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/client/connectedSpaceDataZoneClient.ts new file mode 100644 index 00000000000..cc26dd3f431 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/client/connectedSpaceDataZoneClient.ts @@ -0,0 +1,109 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataZone, ListConnectionsCommandOutput } from '@aws-sdk/client-datazone' +import { getLogger } from '../../../shared/logger/logger' + +/** + * Represents a DataZone connection + */ +export interface DataZoneConnection { + connectionId: string + name: string + type: string + props?: Record +} + +/** + * DataZone client for use in a SageMaker Unified Studio connected space + * Uses the user's current AWS credentials (project role credentials) + */ +export class ConnectedSpaceDataZoneClient { + private datazoneClient: DataZone | undefined + private readonly logger = getLogger('smus') + + constructor( + private readonly region: string, + private readonly customEndpoint?: string + ) {} + + /** + * Gets the DataZone client, initializing it if necessary + * Uses default AWS credentials from the environment (project role) + * Supports custom endpoints for non-production environments + */ + private getDataZoneClient(): DataZone { + if (!this.datazoneClient) { + try { + const clientConfig: any = { + region: this.region, + } + + // Use custom endpoint if provided (for non-prod environments) + if (this.customEndpoint) { + clientConfig.endpoint = this.customEndpoint + this.logger.debug( + `ConnectedSpaceDataZoneClient: Using custom DataZone endpoint: ${this.customEndpoint}` + ) + } else { + this.logger.debug( + `ConnectedSpaceDataZoneClient: Using default AWS DataZone endpoint for region: ${this.region}` + ) + } + + this.logger.debug('ConnectedSpaceDataZoneClient: Creating DataZone client with default credentials') + this.datazoneClient = new DataZone(clientConfig) + this.logger.debug('ConnectedSpaceDataZoneClient: Successfully created DataZone client') + } catch (err) { + this.logger.error('ConnectedSpaceDataZoneClient: Failed to create DataZone client: %s', err as Error) + throw err + } + } + return this.datazoneClient + } + + /** + * Lists the connections in a DataZone domain and project + * @param domainId The DataZone domain identifier + * @param projectId The DataZone project identifier + * @returns List of connections + */ + public async listConnections(domainId: string, projectId: string): Promise { + try { + this.logger.info( + `ConnectedSpaceDataZoneClient: Listing connections for domain ${domainId}, project ${projectId}` + ) + + const datazoneClient = this.getDataZoneClient() + + const response: ListConnectionsCommandOutput = await datazoneClient.listConnections({ + domainIdentifier: domainId, + projectIdentifier: projectId, + }) + + if (!response.items || response.items.length === 0) { + this.logger.info( + `ConnectedSpaceDataZoneClient: No connections found for domain ${domainId}, project ${projectId}` + ) + return [] + } + + const connections: DataZoneConnection[] = response.items.map((connection) => ({ + connectionId: connection.connectionId || '', + name: connection.name || '', + type: connection.type || '', + props: connection.props || {}, + })) + + this.logger.info( + `ConnectedSpaceDataZoneClient: Found ${connections.length} connections for domain ${domainId}, project ${projectId}` + ) + return connections + } catch (err) { + this.logger.error('ConnectedSpaceDataZoneClient: Failed to list connections: %s', err as Error) + throw err + } + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/commands/commands.ts b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/commands/commands.ts new file mode 100644 index 00000000000..01e269004c7 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/commands/commands.ts @@ -0,0 +1,195 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { connectionOptionsService } from '../services/connectionOptionsService' +import { notebookStateManager } from '../services/notebookStateManager' +import { + ConnectionStatusBarProvider, + ProjectStatusBarProvider, + SeparatorStatusBarProvider, +} from '../providers/notebookStatusBarProviders' +import { Constants } from '../models/constants' + +let connectionProvider: ConnectionStatusBarProvider | undefined +let projectProvider: ProjectStatusBarProvider | undefined +let separatorProvider: SeparatorStatusBarProvider | undefined + +/** + * Gets the status bar providers for registration, auto-initializing if needed + */ +export function getStatusBarProviders(): { + connectionProvider: ConnectionStatusBarProvider + projectProvider: ProjectStatusBarProvider + separatorProvider: SeparatorStatusBarProvider +} { + if (!connectionProvider) { + connectionProvider = new ConnectionStatusBarProvider(3, Constants.CONNECTION_COMMAND) + } + if (!projectProvider) { + projectProvider = new ProjectStatusBarProvider(2, Constants.PROJECT_COMMAND) + } + if (!separatorProvider) { + separatorProvider = new SeparatorStatusBarProvider(1) + } + + return { + connectionProvider, + projectProvider, + separatorProvider, + } +} + +/** + * Sets the selected connection for a cell and updates the magic command + */ +export async function setSelectedConnection(cell: vscode.NotebookCell, connectionLabel: string): Promise { + notebookStateManager.setSelectedConnection(cell, connectionLabel, true) + await notebookStateManager.updateCellWithMagic(cell) +} + +/** + * Sets the selected project for a cell and updates the magic command + */ +export async function setSelectedProject(cell: vscode.NotebookCell, projectLabel: string): Promise { + notebookStateManager.setSelectedProject(cell, projectLabel) + await notebookStateManager.updateCellWithMagic(cell) +} + +/** + * Shows a quick pick menu for selecting a connection type and sets the connection for the active cell + */ +export async function showConnectionQuickPick(): Promise { + const editor = vscode.window.activeNotebookEditor + if (!editor) { + return + } + + const cell = editor.selection.start !== undefined ? editor.notebook.cellAt(editor.selection.start) : undefined + if (!cell) { + return + } + + await connectionOptionsService.updateConnectionAndProjectOptions() + + const connectionOptions = connectionOptionsService.getConnectionOptionsSync() + + // Sort connections based on preferred connection order + const sortedOptions = connectionOptions.sort((a, b) => { + // Comparison logic + const aIndex = Constants.CONNECTION_QUICK_PICK_ORDER.indexOf(a.label as any) + const bIndex = Constants.CONNECTION_QUICK_PICK_ORDER.indexOf(b.label as any) + + // If both are in the priority list, sort by their position + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex + } + // If only 'a' is in the priority list, it comes first + if (aIndex !== -1) { + return -1 + } + // If only 'b' is in the priority list, it comes first + if (bIndex !== -1) { + return 1 + } + // If neither is in the priority list, maintain original order + return 0 + }) + + const quickPickItems: vscode.QuickPickItem[] = sortedOptions.map((option) => { + return { + label: option.label, + description: `(${option.magic})`, + iconPath: new vscode.ThemeIcon('plug'), + } + }) + + const selected = await vscode.window.showQuickPick(quickPickItems, { + placeHolder: Constants.CONNECTION_QUICK_PICK_LABEL_PLACEHOLDER, + }) + + if (selected) { + const connectionLabel = selected.detail || selected.label + await setSelectedConnection(cell, connectionLabel) + } +} + +/** + * Shows a quick pick menu for selecting a project type and sets the project for the active cell + */ +export async function showProjectQuickPick(): Promise { + const editor = vscode.window.activeNotebookEditor + if (!editor) { + return + } + + const cell = editor.selection.start !== undefined ? editor.notebook.cellAt(editor.selection.start) : undefined + if (!cell) { + return + } + + const connection = notebookStateManager.getSelectedConnection(cell) + if (!connection) { + return + } + + await connectionOptionsService.updateConnectionAndProjectOptions() + + const options = notebookStateManager.getProjectOptionsForConnection(cell) + if (options.length === 0) { + return + } + + const projectQuickPickItems: vscode.QuickPickItem[] = options.map((option) => { + return { + label: option.project, + description: `(${option.connection})`, + iconPath: new vscode.ThemeIcon('server'), + } + }) + + const selected = await vscode.window.showQuickPick(projectQuickPickItems, { + placeHolder: Constants.PROJECT_QUICK_PICK_LABEL_PLACEHOLDER, + }) + + if (selected) { + if (!selected.label) { + return + } + + await setSelectedProject(cell, selected.label) + } +} + +/** + * Refreshes the status bar items + */ +export function refreshStatusBarItems(): void { + connectionProvider?.refreshCellStatusBar() + projectProvider?.refreshCellStatusBar() + separatorProvider?.refreshCellStatusBar() +} + +/** + * Parses all notebook cells to current cell magics + */ +export async function parseNotebookCells(): Promise { + await connectionOptionsService.updateConnectionAndProjectOptions() + + const editor = vscode.window.activeNotebookEditor + if (!editor) { + return + } + + for (let i = 0; i < editor.notebook.cellCount; i++) { + const cell = editor.notebook.cellAt(i) + + if (cell.kind === vscode.NotebookCellKind.Code && cell.document.languageId !== 'markdown') { + notebookStateManager.parseCellMagic(cell) + } + } + + refreshStatusBarItems() +} diff --git a/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/index.ts b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/index.ts new file mode 100644 index 00000000000..0f7f429b5e6 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/index.ts @@ -0,0 +1,11 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export { activate } from './activation' + +export * from './models/constants' +export * from './models/types' +export * from './services/connectionOptionsService' +export * from './services/notebookStateManager' diff --git a/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/models/constants.ts b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/models/constants.ts new file mode 100644 index 00000000000..d94d4c9f3f7 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/models/constants.ts @@ -0,0 +1,153 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ConnectionTypeProperties } from './types' + +export const Constants = { + // Connection types + CONNECTION_TYPE_EMR_EC2: 'SPARK_EMR_EC2', + CONNECTION_TYPE_EMR_SERVERLESS: 'SPARK_EMR_SERVERLESS', + CONNECTION_TYPE_GLUE: 'SPARK_GLUE', + CONNECTION_TYPE_SPARK: 'SPARK', + CONNECTION_TYPE_REDSHIFT: 'REDSHIFT', + CONNECTION_TYPE_ATHENA: 'ATHENA', + CONNECTION_TYPE_IAM: 'IAM', + + // UI labels and placeholders + CONNECTION_QUICK_PICK_LABEL_PLACEHOLDER: 'Select Connection', + CONNECTION_STATUS_BAR_ITEM_LABEL: 'Select Connection', + CONNECTION_STATUS_BAR_ITEM_ICON: '$(plug)', + DEFAULT_CONNECTION_STATUS_BAR_ITEM_LABEL: 'Connection', + PROJECT_QUICK_PICK_LABEL_PLACEHOLDER: 'Select Compute', + PROJECT_STATUS_BAR_ITEM_LABEL: 'Select Compute', + PROJECT_STATUS_BAR_ITEM_ICON: '$(server)', + DEFAULT_PROJECT_STATUS_BAR_ITEM_LABEL: 'Compute', + CONNECTION_QUICK_PICK_ORDER: ['Local Python', 'PySpark', 'ScalaSpark', 'SQL'] as const, + + // Command IDs + CONNECTION_COMMAND: 'aws.smus.connectionmagics.selectConnection', + PROJECT_COMMAND: 'aws.smus.connectionmagics.selectProject', + + // Magic string literals + LOCAL_PYTHON: 'Local Python', + PYSPARK: 'PySpark', + SCALA_SPARK: 'ScalaSpark', + SQL: 'SQL', + MAGIC_PREFIX: '%%', + LOCAL_MAGIC: '%%local', + NAME_FLAG_LONG: '--name', + NAME_FLAG_SHORT: '-n', + SAGEMAKER_CONNECTION_METADATA_KEY: 'sagemakerConnection', + MARKDOWN_LANGUAGE: 'markdown', + PROJECT_PYTHON: 'project.python', + PROJECT_SPARK_COMPATIBILITY: 'project.spark.compatibility', +} as const + +/** + * Maps connection types to their display properties + */ +export const connectionTypePropertiesMap: Record = { + [Constants.CONNECTION_TYPE_GLUE]: { + labels: ['PySpark', 'SQL'], // Glue supports both PySpark and SQL + magic: '%%pyspark', + language: 'python', + category: 'spark', + }, + [Constants.CONNECTION_TYPE_EMR_EC2]: { + labels: ['PySpark', 'SQL'], // EMR supports both PySpark and SQL + magic: '%%pyspark', + language: 'python', + category: 'spark', + }, + [Constants.CONNECTION_TYPE_EMR_SERVERLESS]: { + labels: ['PySpark', 'SQL'], // EMR supports both PySpark and SQL + magic: '%%pyspark', + language: 'python', + category: 'spark', + }, + [Constants.CONNECTION_TYPE_REDSHIFT]: { + labels: ['SQL'], // Redshift only supports SQL + magic: '%%sql', + language: 'sql', + category: 'sql', + }, + [Constants.CONNECTION_TYPE_ATHENA]: { + labels: ['SQL'], // Athena only supports SQL + magic: '%%sql', + language: 'sql', + category: 'sql', + }, +} + +/** + * Maps connection labels to their display properties + */ +export const connectionLabelPropertiesMap: Record< + string, + { description: string; magic: string; language: string; category: string } +> = { + PySpark: { + description: 'Python with Spark', + magic: '%%pyspark', + language: 'python', + category: 'spark', + }, + SQL: { + description: 'SQL Query', + magic: '%%sql', + language: 'sql', + category: 'sql', + }, + ScalaSpark: { + description: 'Scala with Spark', + magic: '%%scalaspark', + language: 'python', // Scala is not a supported language mode, defaulting to Python + category: 'spark', + }, + 'Local Python': { + description: 'Python', + magic: '%%local', + language: 'python', + category: 'python', + }, + IAM: { + description: 'IAM Connection', + magic: '%%iam', + language: 'python', + category: 'iam', + }, +} + +/** + * Maps connection types to their platform display names for grouping + */ +export const connectionTypeToComputeNameMap: Record = { + [Constants.CONNECTION_TYPE_GLUE]: 'Glue', + [Constants.CONNECTION_TYPE_REDSHIFT]: 'Redshift', + [Constants.CONNECTION_TYPE_ATHENA]: 'Athena', + [Constants.CONNECTION_TYPE_EMR_EC2]: 'EMR EC2', + [Constants.CONNECTION_TYPE_EMR_SERVERLESS]: 'EMR Serverless', +} + +/** + * Maps magic commands to their corresponding connection types + */ +export const magicCommandToConnectionMap: Record = { + '%%spark': 'PySpark', + '%%pyspark': 'PySpark', + '%%scalaspark': 'ScalaSpark', + '%%local': 'Local Python', + '%%sql': 'SQL', +} as const + +/** + * Default project names for each connection type + */ +export const defaultProjectsByConnection: Record = { + 'Local Python': ['project.python'], + PySpark: ['project.spark.compatibility'], + ScalaSpark: ['project.spark.compatibility'], + SQL: ['project.spark.compatibility'], +} as const diff --git a/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/models/types.ts b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/models/types.ts new file mode 100644 index 00000000000..b14daab1ce8 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/models/types.ts @@ -0,0 +1,68 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * SageMaker Connection Summary interface + */ +export interface SageMakerConnectionSummary { + name: string + type: string +} + +/** + * Connection option type definition + */ +export interface ConnectionOption { + label: string + description: string + magic: string + language: string + category: string +} + +/** + * Project option group type definition + */ +export interface ProjectOptionGroup { + connection: string + projects: string[] +} + +/** + * Project option type definition + */ +export interface ProjectOption { + connection: string + project: string +} + +/** + * Connection to project mapping type definition + */ +export interface ConnectionProjectMapping { + connection: string + projectOptions: ProjectOptionGroup[] +} + +/** + * Represents the state of a notebook cell's connection settings + */ +export interface CellState { + connection?: string + project?: string + isUserSelection?: boolean + originalMagicCommand?: string + lastParsedContent?: string +} + +/** + * Maps connection types to their display properties + */ +export interface ConnectionTypeProperties { + labels: string[] + magic: string + language: string + category: string +} diff --git a/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/providers/notebookStatusBarProviders.ts b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/providers/notebookStatusBarProviders.ts new file mode 100644 index 00000000000..8551f615110 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/providers/notebookStatusBarProviders.ts @@ -0,0 +1,143 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { notebookStateManager } from '../services/notebookStateManager' +import { Constants } from '../models/constants' + +/** + * Abstract base class for notebook status bar providers. + */ +export abstract class BaseNotebookStatusBarProvider implements vscode.NotebookCellStatusBarItemProvider { + protected item: vscode.NotebookCellStatusBarItem + protected onDidChangeCellStatusBarItemsEmitter = new vscode.EventEmitter() + protected priority: number + protected icon?: string + protected command?: string + protected tooltip?: string + + public constructor(priority: number, icon?: string, command?: string, tooltip?: string) { + this.priority = priority + this.icon = icon + this.command = command + this.tooltip = tooltip + this.item = new vscode.NotebookCellStatusBarItem('', vscode.NotebookCellStatusBarAlignment.Right) + this.item.priority = priority + } + + /** + * Abstract method that each provider must implement to provide their specific status bar item. + */ + public abstract provideCellStatusBarItems( + cell: vscode.NotebookCell, + token: vscode.CancellationToken + ): vscode.ProviderResult + + /** + * Creates a status bar item with the provided text and applies common settings. + */ + protected createStatusBarItem(text: string, isClickable: boolean = true): vscode.NotebookCellStatusBarItem { + const displayText = this.icon ? `${this.icon} ${text}` : text + const item = new vscode.NotebookCellStatusBarItem(displayText, vscode.NotebookCellStatusBarAlignment.Right) + item.priority = this.priority + + if (isClickable && this.command) { + item.command = this.command + item.tooltip = this.tooltip + } + + return item + } + + /** + * Refreshes the cell status bar items. + */ + public refreshCellStatusBar(): void { + this.onDidChangeCellStatusBarItemsEmitter.fire() + } + + /** + * Event that fires when the cell status bar items have changed. + */ + public get onDidChangeCellStatusBarItems(): vscode.Event { + return this.onDidChangeCellStatusBarItemsEmitter.event + } +} + +/** + * Status bar provider for connection selection in notebook cells. + */ +export class ConnectionStatusBarProvider extends BaseNotebookStatusBarProvider { + public constructor(priority: number, command: string) { + super(priority, Constants.CONNECTION_STATUS_BAR_ITEM_ICON, command, Constants.CONNECTION_STATUS_BAR_ITEM_LABEL) + } + + public provideCellStatusBarItems( + cell: vscode.NotebookCell, + token: vscode.CancellationToken + ): vscode.ProviderResult { + // Don't show on non-code or markdown code cells + if (cell.kind !== vscode.NotebookCellKind.Code || cell.document.languageId === 'markdown') { + return undefined + } + + const connection = notebookStateManager.getSelectedConnection(cell) + + const displayText = connection || Constants.DEFAULT_CONNECTION_STATUS_BAR_ITEM_LABEL + const item = this.createStatusBarItem(displayText) + + return item + } +} + +/** + * Status bar provider for project selection in notebook cells. + */ +export class ProjectStatusBarProvider extends BaseNotebookStatusBarProvider { + public constructor(priority: number, command: string) { + super(priority, Constants.PROJECT_STATUS_BAR_ITEM_ICON, command, Constants.PROJECT_STATUS_BAR_ITEM_LABEL) + } + + public provideCellStatusBarItems( + cell: vscode.NotebookCell, + token: vscode.CancellationToken + ): vscode.ProviderResult { + // Don't show on non-code or markdown code cells + if (cell.kind !== vscode.NotebookCellKind.Code || cell.document.languageId === 'markdown') { + return undefined + } + + const project = notebookStateManager.getSelectedProject(cell) + + const displayText = project || Constants.DEFAULT_PROJECT_STATUS_BAR_ITEM_LABEL + const item = this.createStatusBarItem(displayText) + + return item + } +} + +/** + * Status bar provider for displaying a separator between items in notebook cells. + */ +export class SeparatorStatusBarProvider extends BaseNotebookStatusBarProvider { + public constructor(priority: number, separatorText: string = '|') { + super(priority) + + this.item = new vscode.NotebookCellStatusBarItem(separatorText, vscode.NotebookCellStatusBarAlignment.Right) + this.item.priority = priority + } + + public provideCellStatusBarItems( + cell: vscode.NotebookCell, + token: vscode.CancellationToken + ): vscode.ProviderResult { + // Don't show on non-code or markdown code cells + if (cell.kind !== vscode.NotebookCellKind.Code || cell.document.languageId === 'markdown') { + return undefined + } + + return this.item + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/services/connectionOptionsService.ts b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/services/connectionOptionsService.ts new file mode 100644 index 00000000000..9c258536f68 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/services/connectionOptionsService.ts @@ -0,0 +1,293 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLogger } from '../../../shared/logger/logger' +import { + Constants, + connectionTypePropertiesMap, + connectionLabelPropertiesMap, + connectionTypeToComputeNameMap, +} from '../models/constants' +import { + ConnectionOption, + ProjectOptionGroup, + ConnectionProjectMapping, + SageMakerConnectionSummary, +} from '../models/types' +import { ConnectedSpaceDataZoneClient } from '../client/connectedSpaceDataZoneClient' +import { getResourceMetadata } from '../../shared/utils/resourceMetadataUtils' + +let datazoneClient: ConnectedSpaceDataZoneClient | undefined + +/** + * Gets or creates the module-scoped DataZone client instance + */ +function getDataZoneClient(): ConnectedSpaceDataZoneClient { + if (!datazoneClient) { + const resourceMetadata = getResourceMetadata() + + if (!resourceMetadata?.AdditionalMetadata?.DataZoneDomainRegion) { + throw new Error('DataZone domain region not found in resource metadata') + } + + const region = resourceMetadata.AdditionalMetadata.DataZoneDomainRegion + const customEndpoint = resourceMetadata.AdditionalMetadata?.DataZoneEndpoint + + datazoneClient = new ConnectedSpaceDataZoneClient(region, customEndpoint) + } + return datazoneClient +} + +/** + * Service for managing connection options and project mappings + */ +class ConnectionOptionsService { + private connectionOptions: ConnectionOption[] = [] + private projectOptions: ConnectionProjectMapping[] = [] + private cachedConnections: SageMakerConnectionSummary[] = [] + + constructor() {} + + /** + * Gets the appropriate connection option for a given label + */ + private getConnectionOptionForLabel(label: string): ConnectionOption | undefined { + const labelProps = connectionLabelPropertiesMap[label] + if (!labelProps) { + return undefined + } + + return { + label, + description: labelProps.description, + magic: labelProps.magic, + language: labelProps.language, + category: labelProps.category, + } + } + + /** + * Gets filtered connections from DataZone, excluding IAM connections and processing SPARK connections + */ + private async getFilteredConnections(forceRefresh: boolean = false): Promise { + if (this.cachedConnections.length > 0 && !forceRefresh) { + return this.cachedConnections + } + + try { + const resourceMetadata = getResourceMetadata() + + if (!resourceMetadata?.AdditionalMetadata?.DataZoneDomainId) { + throw new Error('DataZone domain ID not found in resource metadata') + } + + if (!resourceMetadata?.AdditionalMetadata?.DataZoneProjectId) { + throw new Error('DataZone project ID not found in resource metadata') + } + + const connections = await getDataZoneClient().listConnections( + resourceMetadata.AdditionalMetadata.DataZoneDomainId, + resourceMetadata.AdditionalMetadata.DataZoneProjectId + ) + + const processedConnections: SageMakerConnectionSummary[] = [] + + for (const connection of connections) { + if ( + connection.type === Constants.CONNECTION_TYPE_REDSHIFT || + connection.type === Constants.CONNECTION_TYPE_ATHENA + ) { + processedConnections.push({ + name: connection.name || '', + type: connection.type || '', + }) + } else if (connection.type === Constants.CONNECTION_TYPE_SPARK) { + if ('sparkGlueProperties' in (connection.props || {})) { + processedConnections.push({ + name: connection.name || '', + type: Constants.CONNECTION_TYPE_GLUE, + }) + } else if ( + 'sparkEmrProperties' in (connection.props || {}) && + 'computeArn' in (connection.props?.sparkEmrProperties || {}) + ) { + const computeArn = connection.props?.sparkEmrProperties?.computeArn || '' + + if (computeArn.includes('cluster')) { + processedConnections.push({ + name: connection.name || '', + type: Constants.CONNECTION_TYPE_EMR_EC2, + }) + } else if (computeArn.includes('applications')) { + processedConnections.push({ + name: connection.name || '', + type: Constants.CONNECTION_TYPE_EMR_SERVERLESS, + }) + } + } + } + } + + this.cachedConnections = processedConnections + return processedConnections + } catch (error) { + getLogger('smus').error('Failed to list DataZone connections: %s', error as Error) + return [] + } + } + + /** + * Adds custom Local Python option to the options list + */ + private addLocalPythonOption(options: ConnectionOption[], addedLabels: Set): void { + const localPythonOption = this.getConnectionOptionForLabel('Local Python') + if (localPythonOption) { + options.push(localPythonOption) + addedLabels.add('Local Python') + } + } + + /** + * Gets the available connection options, either from DataZone connections or defaults + * @returns Array of connection options + */ + public async getConnectionOptions(): Promise { + try { + const connections = await this.getFilteredConnections() + + if (connections.length === 0) { + return [] + } + + const options: ConnectionOption[] = [] + const addedLabels = new Set() + + this.addLocalPythonOption(options, addedLabels) + + for (const connection of connections) { + const typeProps = connectionTypePropertiesMap[connection.type] + if (typeProps) { + for (const label of typeProps.labels) { + if (!addedLabels.has(label)) { + const connectionOption = this.getConnectionOptionForLabel(label) + if (connectionOption) { + options.push(connectionOption) + addedLabels.add(label) + } + } + } + } + } + + if (addedLabels.has(Constants.PYSPARK) && !addedLabels.has(Constants.SCALA_SPARK)) { + const scalaSparkOption = this.getConnectionOptionForLabel(Constants.SCALA_SPARK) + if (scalaSparkOption) { + options.push(scalaSparkOption) + } + } + + return options + } catch (error) { + getLogger('smus').error('Failed to get connection options: %s', error as Error) + return [] + } + } + + /** + * Gets the project options for a specific connection type + * @param connectionType The connection type + * @returns Project options for the connection type + */ + public async getProjectOptionsForConnectionType(connectionType: string): Promise { + try { + const connections = await this.getFilteredConnections() + + if (connections.length === 0) { + return [] + } + + const effectiveConnectionType = connectionType === 'ScalaSpark' ? 'PySpark' : connectionType + const filteredConnections: Record = {} + + for (const connection of connections) { + const typeProps = connectionTypePropertiesMap[connection.type] + + if (typeProps && typeProps.labels.includes(effectiveConnectionType)) { + const compute = connectionTypeToComputeNameMap[connection.type] || 'Unknown' + + if (!filteredConnections[compute]) { + filteredConnections[compute] = [] + } + filteredConnections[compute].push(connection.name) + } + } + + const projectOptions: ProjectOptionGroup[] = [] + for (const [compute, projects] of Object.entries(filteredConnections)) { + projectOptions.push({ connection: compute, projects }) + } + + return projectOptions + } catch (error) { + getLogger('smus').error('Failed to get project options: %s', error as Error) + return [] + } + } + + /** + * Updates the connection and project options from DataZone + */ + public async updateConnectionAndProjectOptions(): Promise { + try { + this.connectionOptions = await this.getConnectionOptions() + + if (this.connectionOptions.length === 0) { + this.projectOptions = [] + return + } + + const newProjectOptions: ConnectionProjectMapping[] = [] + + newProjectOptions.push({ + connection: 'Local Python', + projectOptions: [{ connection: 'Local', projects: ['project.python'] }], + }) + + for (const option of this.connectionOptions) { + if (option.label !== 'Local Python') { + const projectOpts = await this.getProjectOptionsForConnectionType(option.label) + if (projectOpts.length > 0) { + newProjectOptions.push({ + connection: option.label, + projectOptions: projectOpts, + }) + } + } + } + + this.projectOptions = newProjectOptions + } catch (error) { + getLogger('smus').error('Failed to update connection and project options: %s', error as Error) + this.connectionOptions = [] + this.projectOptions = [] + } + } + + /** + * Gets the current cached connection options + */ + public getConnectionOptionsSync(): ConnectionOption[] { + return this.connectionOptions + } + + /** + * Gets the current cached project options + */ + public getProjectOptionsSync(): ConnectionProjectMapping[] { + return this.projectOptions + } +} + +export const connectionOptionsService = new ConnectionOptionsService() diff --git a/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/services/notebookStateManager.ts b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/services/notebookStateManager.ts new file mode 100644 index 00000000000..f2d9ec2392b --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/services/notebookStateManager.ts @@ -0,0 +1,422 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { CellState, ProjectOption } from '../models/types' +import { connectionOptionsService } from './connectionOptionsService' +import { getLogger } from '../../../shared/logger/logger' +import { magicCommandToConnectionMap, defaultProjectsByConnection, Constants } from '../models/constants' + +/** + * State manager for tracking notebook cell states and selections + */ +class NotebookStateManager { + private cellStates: Map = new Map() + + constructor() {} + + /** + * Gets the cell state for a specific cell + */ + private getCellState(cell: vscode.NotebookCell): CellState { + const cellId = cell.document.uri.toString() + if (!this.cellStates.has(cellId)) { + this.cellStates.set(cellId, {}) + } + return this.cellStates.get(cellId)! + } + + /** + * Sets metadata on a cell + */ + private async setCellMetadata(cell: vscode.NotebookCell, key: string, value: any): Promise { + try { + const edit = new vscode.WorkspaceEdit() + const notebookEdit = vscode.NotebookEdit.updateCellMetadata(cell.index, { + ...cell.metadata, + [key]: value, + }) + edit.set(cell.notebook.uri, [notebookEdit]) + await vscode.workspace.applyEdit(edit) + } catch (error) { + getLogger('smus').warn('setCellMetadata: Failed to set metadata, falling back to in-memory storage') + } + } + + /** + * Gets the selected connection for a cell + */ + public getSelectedConnection(cell: vscode.NotebookCell): string | undefined { + const connection = cell.metadata?.[Constants.SAGEMAKER_CONNECTION_METADATA_KEY] as string + if (connection) { + return connection + } + + const state = this.getCellState(cell) + const currentCellContent = cell.document.getText() + + if (!state.connection || (!state.isUserSelection && state.lastParsedContent !== currentCellContent)) { + this.parseCellMagic(cell) + const updatedState = this.getCellState(cell) + updatedState.lastParsedContent = currentCellContent + + return updatedState.connection + } + + return state.connection + } + + /** + * Sets the selected connection for a cell + */ + public setSelectedConnection( + cell: vscode.NotebookCell, + value: string | undefined, + isUserSelection: boolean = false + ): void { + const state = this.getCellState(cell) + const previousConnection = state.connection + state.connection = value + + if (isUserSelection) { + state.isUserSelection = true + + if (value) { + void this.setCellMetadata(cell, Constants.SAGEMAKER_CONNECTION_METADATA_KEY, value) + } + } + + if (value === Constants.LOCAL_PYTHON || value === undefined) { + if (value === Constants.LOCAL_PYTHON && previousConnection !== value) { + state.project = undefined + this.setDefaultProjectForConnection(cell, Constants.LOCAL_PYTHON) + } else if (value === Constants.LOCAL_PYTHON && previousConnection === value) { + if (!state.project) { + this.setDefaultProjectForConnection(cell, Constants.LOCAL_PYTHON) + } + } else { + state.project = undefined + } + } else if (previousConnection !== value) { + state.project = undefined + this.setDefaultProjectForConnection(cell, value) + } + } + + /** + * Gets the selected project for a cell + */ + public getSelectedProject(cell: vscode.NotebookCell): string | undefined { + return this.getCellState(cell).project + } + + /** + * Sets the selected project for a cell + */ + public setSelectedProject(cell: vscode.NotebookCell, value: string | undefined): void { + const state = this.getCellState(cell) + state.project = value + } + + /** + * Gets the magic command for a cell using simplified format for UI operations + */ + public getMagicCommand(cell: vscode.NotebookCell): string | undefined { + const connection = this.getSelectedConnection(cell) + if (!connection) { + return + } + + if (connection === Constants.LOCAL_PYTHON) { + const state = this.getCellState(cell) + const hasLocalMagic = state.originalMagicCommand?.startsWith(Constants.LOCAL_MAGIC) + + if (!hasLocalMagic) { + return undefined + } + } + + const connectionOptions = connectionOptionsService.getConnectionOptionsSync() + + const connectionOption = connectionOptions.find((option) => option.label === connection) + if (!connectionOption) { + return undefined + } + + const project = this.getSelectedProject(cell) + + if (!project) { + return connectionOption.magic + } + + return `${connectionOption.magic} ${project}` + } + + /** + * Parses a cell's content to detect magic commands and updates the state manager + * @param cell The notebook cell to parse + */ + public parseCellMagic(cell: vscode.NotebookCell): void { + if ( + !cell || + cell.kind !== vscode.NotebookCellKind.Code || + cell.document.languageId === Constants.MARKDOWN_LANGUAGE + ) { + return + } + + const state = this.getCellState(cell) + if (state.isUserSelection) { + return + } + + const cellText = cell.document.getText() + const lines = cellText.split('\n') + + const firstLine = lines[0].trim() + if (!firstLine.startsWith(Constants.MAGIC_PREFIX)) { + this.setSelectedConnection(cell, Constants.LOCAL_PYTHON) + return + } + + const parsed = this.parseMagicCommandLine(firstLine) + if (!parsed) { + return + } + + const connectionType = magicCommandToConnectionMap[parsed.magic] + if (!connectionType) { + this.setSelectedConnection(cell, Constants.LOCAL_PYTHON) + this.setDefaultProjectForConnection(cell, Constants.LOCAL_PYTHON) + return + } + + const cellState = this.getCellState(cell) + cellState.originalMagicCommand = firstLine + + this.setSelectedConnection(cell, connectionType) + + if (parsed.project) { + this.setSelectedProject(cell, parsed.project) + } else { + this.setDefaultProjectForConnection(cell, connectionType) + } + } + + /** + * Parses a magic command line to extract magic and project parameters + * Supports formats: %%magic, %%magic project, %%magic --name project, %%magic -n project + */ + private parseMagicCommandLine(line: string): { magic: string; project?: string } | undefined { + const tokens = line.split(/\s+/) + if (tokens.length === 0 || !tokens[0].startsWith(Constants.MAGIC_PREFIX)) { + return undefined + } + + const magic = tokens[0] + let project: string | undefined + + if (tokens.length === 2) { + // Format: %%magic project + project = tokens[1] + } else if (tokens.length >= 3) { + // Format: %%magic --name project or %%magic -n project + const flagIndex = tokens.findIndex( + (token) => token === Constants.NAME_FLAG_LONG || token === Constants.NAME_FLAG_SHORT + ) + if (flagIndex !== -1 && flagIndex + 1 < tokens.length) { + project = tokens[flagIndex + 1] + } + } + + return { magic, project } + } + + /** + * Sets default project for a connection when no explicit project is specified + */ + private setDefaultProjectForConnection(cell: vscode.NotebookCell, connectionType: string): void { + const projectOptions = connectionOptionsService.getProjectOptionsSync() + + const mapping = projectOptions.find((option) => option.connection === connectionType) + if (!mapping || mapping.projectOptions.length === 0) { + return + } + + const defaultProjects = defaultProjectsByConnection[connectionType] || [] + + for (const defaultProject of defaultProjects) { + for (const projectOption of mapping.projectOptions) { + if (projectOption.projects.includes(defaultProject)) { + this.setSelectedProject(cell, defaultProject) + return + } + } + } + + const firstProjectOption = mapping.projectOptions[0] + if (firstProjectOption.projects.length > 0) { + this.setSelectedProject(cell, firstProjectOption.projects[0]) + } + } + + /** + * Updates the current cell with the magic command and sets the cell language + * @param cell The notebook cell to update + */ + public async updateCellWithMagic(cell: vscode.NotebookCell): Promise { + const connection = this.getSelectedConnection(cell) + if (!connection) { + return + } + + const connectionOptions = connectionOptionsService.getConnectionOptionsSync() + const connectionOption = connectionOptions.find((option) => option.label === connection) + if (!connectionOption) { + return + } + + try { + await vscode.languages.setTextDocumentLanguage(cell.document, connectionOption.language) + + const cellText = cell.document.getText() + const lines = cellText.split('\n') + const firstLine = lines[0] || '' + const isMagicCommand = firstLine.trim().startsWith(Constants.MAGIC_PREFIX) + + let newCellContent = cellText + + if (connection === Constants.LOCAL_PYTHON) { + const state = this.getCellState(cell) + const hasLocalMagic = state.originalMagicCommand?.startsWith(Constants.LOCAL_MAGIC) + + if (hasLocalMagic) { + const magicCommand = this.getMagicCommand(cell) + if (magicCommand) { + if (isMagicCommand) { + newCellContent = magicCommand + '\n' + lines.slice(1).join('\n') + } else { + newCellContent = magicCommand + '\n' + cellText + } + } + } else { + if (isMagicCommand) { + newCellContent = lines.slice(1).join('\n') + } + } + } else { + const magicCommand = this.getMagicCommand(cell) + + if (magicCommand) { + if (!magicCommand.startsWith(Constants.MAGIC_PREFIX)) { + return + } + + if (isMagicCommand) { + newCellContent = magicCommand + '\n' + lines.slice(1).join('\n') + } else { + newCellContent = magicCommand + '\n' + cellText + } + } + } + + if (newCellContent !== cellText) { + await this.updateCellContent(cell, newCellContent) + } + } catch (error) { + getLogger('smus').error(`Error updating cell with magic command: ${error}`) + } + } + + /** + * Updates the content of a notebook cell using the most appropriate API for the environment + * @param cell The notebook cell to update + * @param newContent The new content for the cell + */ + private async updateCellContent(cell: vscode.NotebookCell, newContent: string): Promise { + try { + if (vscode.workspace.applyEdit && (vscode as any).NotebookEdit) { + const edit = new vscode.WorkspaceEdit() + const notebookUri = cell.notebook.uri + const cellIndex = cell.index + + const newCellData = new vscode.NotebookCellData(cell.kind, newContent, cell.document.languageId) + + const notebookEdit = (vscode as any).NotebookEdit.replaceCells( + new vscode.NotebookRange(cellIndex, cellIndex + 1), + [newCellData] + ) + edit.set(notebookUri, [notebookEdit]) + + const success = await vscode.workspace.applyEdit(edit) + if (success) { + return + } + } + } catch (error) { + getLogger('smus').error( + `NotebookEdit failed, attempting to update cell content with WorkspaceEdit: ${error}` + ) + } + + try { + const edit = new vscode.WorkspaceEdit() + + const fullRange = new vscode.Range( + new vscode.Position(0, 0), + new vscode.Position(cell.document.lineCount, 0) + ) + + edit.replace(cell.document.uri, fullRange, newContent) + + const success = await vscode.workspace.applyEdit(edit) + if (!success) { + getLogger('smus').error('WorkspaceEdit failed to apply') + } + } catch (error) { + getLogger('smus').error(`Failed to update cell content with WorkspaceEdit: ${error}`) + + try { + const document = cell.document + if (document && 'getText' in document && 'uri' in document) { + const edit = new vscode.WorkspaceEdit() + const fullText = document.getText() + const fullRange = new vscode.Range(document.positionAt(0), document.positionAt(fullText.length)) + edit.replace(document.uri, fullRange, newContent) + await vscode.workspace.applyEdit(edit) + } + } catch (finalError) { + getLogger('smus').error(`All cell update methods failed: ${finalError}`) + } + } + } + + /** + * Gets the project options for the selected connection in a cell + */ + public getProjectOptionsForConnection(cell: vscode.NotebookCell): ProjectOption[] { + const connection = this.getSelectedConnection(cell) + if (!connection) { + return [] + } + + const projectOptions = connectionOptionsService.getProjectOptionsSync() + const mapping = projectOptions.find((option) => option.connection === connection) + if (!mapping) { + return [] + } + + const options: ProjectOption[] = [] + for (const projectOption of mapping.projectOptions) { + for (const project of projectOption.projects) { + options.push({ connection: projectOption.connection, project: project }) + } + } + + return options + } +} + +export const notebookStateManager = new NotebookStateManager() diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/activation.ts b/packages/core/src/sagemakerunifiedstudio/explorer/activation.ts new file mode 100644 index 00000000000..2b05e7be278 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/activation.ts @@ -0,0 +1,182 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { ResourceTreeDataProvider } from '../../shared/treeview/resourceTreeDataProvider' +import { + smusLoginCommand, + smusLearnMoreCommand, + smusSignOutCommand, + SageMakerUnifiedStudioRootNode, + selectSMUSProject, +} from './nodes/sageMakerUnifiedStudioRootNode' +import { openRemoteConnect, stopSpace } from '../../awsService/sagemaker/commands' +import { SagemakerUnifiedStudioSpaceNode } from './nodes/sageMakerUnifiedStudioSpaceNode' +import { SageMakerUnifiedStudioProjectNode } from './nodes/sageMakerUnifiedStudioProjectNode' +import { getLogger } from '../../shared/logger/logger' +import { setSmusConnectedContext, SmusAuthenticationProvider } from '../auth/providers/smusAuthenticationProvider' +import { isSmusIamConnection } from '../auth/model' +import { setupUserActivityMonitoring } from '../../awsService/sagemaker/sagemakerSpace' +import { telemetry } from '../../shared/telemetry/telemetry' +import { isSageMaker } from '../../shared/extensionUtilities' +import { recordSpaceTelemetry } from '../shared/telemetry' +import { DataZoneClient } from '../shared/client/datazoneClient' +import { handleCredExpiredError } from '../shared/credentialExpiryHandler' + +export async function activate(extensionContext: vscode.ExtensionContext): Promise { + // Initialize the SMUS authentication provider + const logger = getLogger('smus') + logger.debug('Initializing authentication provider') + // Create the auth provider instance (this will trigger restore() in the constructor) + const smusAuthProvider = SmusAuthenticationProvider.fromContext() + await smusAuthProvider.restore() + // Set initial auth context after restore + void setSmusConnectedContext(smusAuthProvider.isConnected()) + logger.debug('Authentication provider initialized') + + // Create the SMUS projects tree view + const smusRootNode = new SageMakerUnifiedStudioRootNode(smusAuthProvider, extensionContext) + const treeDataProvider = new ResourceTreeDataProvider({ getChildren: () => smusRootNode.getChildren() }) + + // Register the tree view + const treeView = vscode.window.createTreeView('aws.smus.rootView', { treeDataProvider }) + treeDataProvider.refresh() + + // Register the commands + extensionContext.subscriptions.push( + smusLoginCommand.register(extensionContext), + smusLearnMoreCommand.register(), + smusSignOutCommand.register(extensionContext), + treeView, + vscode.commands.registerCommand('aws.smus.rootView.refresh', () => { + treeDataProvider.refresh() + }), + + vscode.commands.registerCommand( + 'aws.smus.projectView', + async (projectNode?: SageMakerUnifiedStudioProjectNode) => { + return await selectSMUSProject(projectNode) + } + ), + + vscode.commands.registerCommand('aws.smus.refreshProject', async () => { + const projectNode = smusRootNode.getProjectSelectNode() + await projectNode.refreshNode() + }), + + vscode.commands.registerCommand('aws.smus.refresh', async () => { + treeDataProvider.refresh() + }), + + vscode.commands.registerCommand('aws.smus.switchProject', async () => { + // Get the project node from the root node to ensure we're using the same instance + const projectNode = smusRootNode.getProjectSelectNode() + return await selectSMUSProject(projectNode) + }), + + vscode.commands.registerCommand('aws.smus.stopSpace', async (node: SagemakerUnifiedStudioSpaceNode) => { + if (!validateNode(node)) { + return + } + await telemetry.smus_stopSpace.run(async (span) => { + try { + await recordSpaceTelemetry(span, node) + await stopSpace(node.resource, extensionContext, node.resource.sageMakerClient) + } catch (err) { + await handleCredExpiredError(err) + throw err + } + }) + }), + + vscode.commands.registerCommand( + 'aws.smus.openRemoteConnection', + async (node: SagemakerUnifiedStudioSpaceNode) => { + if (!validateNode(node)) { + return + } + await telemetry.smus_openRemoteConnection.run(async (span) => { + try { + await recordSpaceTelemetry(span, node) + await openRemoteConnect(node.resource, extensionContext, node.resource.sageMakerClient) + } catch (err) { + await handleCredExpiredError(err) + throw err + } + }) + } + ), + + vscode.commands.registerCommand('aws.smus.reauthenticate', async (connection?: any) => { + if (connection) { + try { + await smusAuthProvider.reauthenticate(connection) + const projectNode = smusRootNode.getProjectSelectNode() + if (projectNode) { + const project = projectNode.getProject() + if (!project) { + await vscode.commands.executeCommand('aws.smus.switchProject') + } + } + treeDataProvider.refresh() + + // IAM connections handle their own success messages + // Only show success message for SSO connections + if (!isSmusIamConnection(connection)) { + void vscode.window.showInformationMessage( + 'Successfully reauthenticated with SageMaker Unified Studio' + ) + } + } catch (error) { + // Extract the most detailed error message available + let errorMessage = 'Unknown error' + if (error instanceof Error) { + // Check if this is a ToolkitError with a cause chain + const cause = (error as any).cause + if (cause instanceof Error) { + // Use the cause's message as it contains the detailed validation error + errorMessage = cause.message + } else { + // Fall back to the error's own message + errorMessage = error.message + } + } + + // Show the detailed error message to the user + void vscode.window.showErrorMessage(`${errorMessage}`) + logger.error('Reauthentication failed: %O', error) + } + } + }), + // Dispose DataZoneClient when extension is deactivated + { dispose: () => DataZoneClient.dispose() }, + // Dispose SMUS auth provider when extension is deactivated + { dispose: () => smusAuthProvider.dispose() } + ) + + // Track user activity for autoshutdown feature when in SageMaker Unified Studio environment + if (isSageMaker('SMUS-SPACE-REMOTE-ACCESS')) { + logger.info('SageMaker Unified Studio environment detected, setting up user activity monitoring') + try { + await setupUserActivityMonitoring(extensionContext) + } catch (error) { + logger.error(`Error in UserActivityMonitoring: ${error}`) + throw error + } + } else { + logger.info('Not in SageMaker Unified Studio remote environment, skipping user activity monitoring') + } +} + +/** + * Checks if a node is undefined and shows a warning message if so. + */ +function validateNode(node: SagemakerUnifiedStudioSpaceNode): boolean { + if (!node) { + void vscode.window.showWarningMessage('Space information is being refreshed. Please try again shortly.') + return false + } + return true +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/federatedConnectionStrategy.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/federatedConnectionStrategy.ts new file mode 100644 index 00000000000..467509cefa7 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/federatedConnectionStrategy.ts @@ -0,0 +1,333 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { getLogger } from '../../../shared/logger/logger' +import { DataZoneConnection } from '../../shared/client/datazoneClient' +import { GlueClient, ListEntitiesCommand, DescribeEntityCommand, Entity, Field } from '@aws-sdk/client-glue' +import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider' +import { getIcon } from '../../../shared/icons' +import { createPlaceholderItem } from '../../../shared/treeview/utils' +import { createErrorItem, createColumnTreeItem } from './utils' +import { NO_DATA_FOUND_MESSAGE, NodeType } from './types' +import { handleCredExpiredError } from '../../shared/credentialExpiryHandler' + +/** + * Creates a federated connection node + */ +export async function createFederatedConnectionNode( + connection: DataZoneConnection, + connectionCredentialsProvider: ConnectionCredentialsProvider, + region: string +): Promise { + const logger = getLogger('smus') + + // Check for error message in glue properties + // Create error node directly in this case + const connectionError = connection.props?.glueProperties?.errorMessage + if (connectionError) { + return createErrorItem(connectionError, 'glue-error', connection.connectionId) + } + + return { + id: `federated-${connection.connectionId}`, + resource: connection, + getTreeItem: () => { + const item = new vscode.TreeItem(connection.name, vscode.TreeItemCollapsibleState.Collapsed) + item.contextValue = 'federatedConnection' + item.iconPath = getIcon('aws-sagemakerunifiedstudio-catalog') + item.tooltip = `Federated Connection: ${connection.name}` + return item + }, + getChildren: async () => { + try { + return await getFederatedEntities(connection, connectionCredentialsProvider, region) + } catch (err) { + logger.error(`Failed to get federated entities: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [ + createErrorItem(`Failed to load entities - ${errorMessage}`, 'entities', connection.connectionId), + ] + } + }, + getParent: () => undefined, + } +} + +/** + * Gets federated entities from Glue API + */ +async function getFederatedEntities( + connection: DataZoneConnection, + connectionCredentialsProvider: ConnectionCredentialsProvider, + region: string +): Promise { + const awsCredentialProvider = async () => { + const credentials = await connectionCredentialsProvider.getCredentials() + return { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.sessionToken, + expiration: credentials.expiration, + } + } + const glueClient = new GlueClient({ + region: region, + credentials: awsCredentialProvider, + }) + + const glueConnectionName = connection?.glueConnectionName + if (!glueConnectionName) { + return [createErrorItem('No Glue connection name found', 'glue-connection', connection.connectionId)] + } + + const allEntities: Entity[] = [] + let nextToken: string | undefined + + do { + const response = await glueClient.send( + new ListEntitiesCommand({ + ConnectionName: glueConnectionName, + NextToken: nextToken, + }) + ) + + if (response.Entities) { + allEntities.push(...response.Entities) + } + nextToken = response.NextToken + } while (nextToken) + + if (allEntities.length === 0) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE)] + } + + const entityNodes: TreeNode[] = [] + const tableNodes: TreeNode[] = [] + + for (const entity of allEntities) { + const nodeType = getGlueNodeType(entity.Category) + const isTable = nodeType === NodeType.GLUE_TABLE + + const entityNode = createGlueEntityNode(entity, connection, glueClient, glueConnectionName) + + if (isTable) { + tableNodes.push(entityNode) + } else { + entityNodes.push(entityNode) + } + } + + // Always group tables under a "Tables" container + if (tableNodes.length > 0) { + const tablesContainer = createTablesContainer(tableNodes, connection.connectionId) + return [...entityNodes, tablesContainer] + } + + return entityNodes +} + +/** + * Creates a Glue entity node + */ +function createGlueEntityNode( + entity: Entity, + connection: DataZoneConnection, + glueClient: GlueClient, + glueConnectionName: string +): TreeNode { + const logger = getLogger('smus') + const nodeType = getGlueNodeType(entity.Category) + const isTable = nodeType === NodeType.GLUE_TABLE + + return { + id: `${connection.connectionId}-${entity.EntityName}`, + resource: entity, + getTreeItem: () => { + const item = new vscode.TreeItem( + entity.Label || entity.EntityName || 'Unknown', + entity.IsParentEntity || (isTable && !entity.IsParentEntity) + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None + ) + item.contextValue = nodeType + item.iconPath = getGlueEntityIcon(nodeType) + item.tooltip = `${entity.Category}: ${entity.Label || entity.EntityName}` + return item + }, + getChildren: async () => { + try { + if (entity.IsParentEntity) { + return await getChildEntities(entity, connection, glueClient, glueConnectionName) + } else if (isTable) { + return await getTableColumns(entity, glueClient, glueConnectionName) + } + return [] + } catch (err) { + logger.error(`Failed to get children for entity ${entity.EntityName}: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [ + createErrorItem( + `Failed to load children - ${errorMessage}`, + 'entity-children', + entity.EntityName || 'unknown' + ), + ] + } + }, + getParent: () => undefined, + } +} + +/** + * Gets child entities for parent entities + */ +async function getChildEntities( + parentEntity: Entity, + connection: DataZoneConnection, + glueClient: GlueClient, + glueConnectionName: string +): Promise { + const allEntities: Entity[] = [] + let nextToken: string | undefined + + do { + const response = await glueClient.send( + new ListEntitiesCommand({ + ConnectionName: glueConnectionName, + ParentEntityName: parentEntity.EntityName, + NextToken: nextToken, + }) + ) + + if (response.Entities) { + allEntities.push(...response.Entities) + } + nextToken = response.NextToken + } while (nextToken) + + if (allEntities.length === 0) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE)] + } + + const entityNodes: TreeNode[] = [] + const tableNodes: TreeNode[] = [] + + for (const entity of allEntities) { + const nodeType = getGlueNodeType(entity.Category) + const isTable = nodeType === NodeType.GLUE_TABLE + const entityNode = createGlueEntityNode(entity, connection, glueClient, glueConnectionName) + + if (isTable) { + tableNodes.push(entityNode) + } else { + entityNodes.push(entityNode) + } + } + + // Always group tables under a "Tables" container if there are any + if (tableNodes.length > 0) { + const tablesContainer = createTablesContainer( + tableNodes, + `${connection.connectionId}-${parentEntity.EntityName}` + ) + return [...entityNodes, tablesContainer] + } + + return entityNodes +} + +/** + * Gets table columns using DescribeEntity + */ +async function getTableColumns( + entity: Entity, + glueClient: GlueClient, + glueConnectionName: string +): Promise { + const response = await glueClient.send( + new DescribeEntityCommand({ + ConnectionName: glueConnectionName, + EntityName: entity.EntityName, + }) + ) + + if (!response.Fields || response.Fields.length === 0) { + return [createPlaceholderItem('No columns found')] + } + + return response.Fields.map((field) => createColumnNode(field, entity.EntityName || 'unknown')) +} + +/** + * Creates a column node + */ +function createColumnNode(field: Field, tableName: string): TreeNode { + return { + id: `${tableName}-${field.FieldName}`, + resource: field, + getTreeItem: () => { + return createColumnTreeItem( + field.Label || field.FieldName || 'Unknown', + field.FieldType || 'unknown', + NodeType.REDSHIFT_COLUMN + ) + }, + getChildren: async () => [], + getParent: () => undefined, + } +} + +/** + * Creates a tables container node + */ +function createTablesContainer(tableNodes: TreeNode[], connectionId: string): TreeNode { + return { + id: `${connectionId}-tables`, + resource: {}, + getTreeItem: () => { + const item = new vscode.TreeItem('Tables', vscode.TreeItemCollapsibleState.Collapsed) + item.contextValue = NodeType.GLUE_TABLE + item.iconPath = new vscode.ThemeIcon('table') + return item + }, + getChildren: async () => tableNodes, + getParent: () => undefined, + } +} + +/** + * Maps Glue entity category to node type + */ +function getGlueNodeType(category?: string): NodeType { + const lowerCategory = category?.toLowerCase() + if (lowerCategory?.includes('schema')) { + return NodeType.GLUE_DATABASE + } else if (lowerCategory?.includes('table')) { + return NodeType.GLUE_TABLE + } else if (lowerCategory?.includes('database')) { + return NodeType.GLUE_DATABASE + } + return NodeType.GLUE_CATALOG +} + +/** + * Gets icon for Glue entity node type + */ +function getGlueEntityIcon(nodeType: NodeType): vscode.ThemeIcon | any { + switch (nodeType) { + case NodeType.GLUE_DATABASE: + return new vscode.ThemeIcon('database') + case NodeType.GLUE_TABLE: + return getIcon('aws-redshift-table') + case NodeType.GLUE_CATALOG: + return getIcon('aws-sagemakerunifiedstudio-catalog') + default: + return getIcon('vscode-circle-outline') + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy.ts new file mode 100644 index 00000000000..123f92c1eb4 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy.ts @@ -0,0 +1,602 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { getLogger } from '../../../shared/logger/logger' +import { DataZoneConnection } from '../../shared/client/datazoneClient' +import { Catalog } from '@amzn/glue-catalog-client' +import { GlueClient } from '../../shared/client/glueClient' +import { ConnectionClientStore } from '../../shared/client/connectionClientStore' +import { + NODE_ID_DELIMITER, + NodeType, + NodeData, + DATA_DEFAULT_LAKEHOUSE_CONNECTION_NAME_REGEXP, + DATA_DEFAULT_ATHENA_CONNECTION_NAME_REGEXP, + DATA_DEFAULT_IAM_CONNECTION_NAME_REGEXP, + AWS_DATA_CATALOG, + DatabaseObjects, + NO_DATA_FOUND_MESSAGE, +} from './types' +import { handleCredExpiredError } from '../../shared/credentialExpiryHandler' +import { + getLabel, + isLeafNode, + getIconForNodeType, + getTooltip, + createColumnTreeItem, + getColumnType, + createErrorItem, + isRedLakeCatalog, + isS3TablesCatalog, +} from './utils' +import { createPlaceholderItem } from '../../../shared/treeview/utils' +import { Column, Database, Table } from '@aws-sdk/client-glue' +import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider' +import { telemetry } from '../../../shared/telemetry/telemetry' +import { recordDataConnectionTelemetry } from '../../shared/telemetry' +import { GlueCatalogClient } from '../../shared/client/glueCatalogClient' + +/** + * Lakehouse data node for SageMaker Unified Studio + */ +export class LakehouseNode implements TreeNode { + private childrenNodes: TreeNode[] | undefined + private isLoading = false + private readonly logger = getLogger('smus') + + constructor( + public readonly data: NodeData, + private readonly childrenProvider?: (node: LakehouseNode) => Promise + ) {} + + public get id(): string { + return this.data.id + } + + public get resource(): any { + return this.data.value || {} + } + + public async getChildren(): Promise { + // Return cached children if available + if (this.childrenNodes && !this.isLoading) { + return this.childrenNodes + } + + // Return empty array for leaf nodes + if (isLeafNode(this.data)) { + return [] + } + + // If we have a children provider, use it + if (this.childrenProvider) { + try { + this.isLoading = true + const childrenNodes = await this.childrenProvider(this) + this.childrenNodes = childrenNodes + this.isLoading = false + return this.childrenNodes + } catch (err) { + this.isLoading = false + this.logger.error(`Failed to get children for node ${this.data.id}: ${(err as Error).message}`) + + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'getChildren', this.id) as LakehouseNode] + } + } + + return [] + } + + public async getTreeItem(): Promise { + const label = getLabel(this.data) + const isLeaf = isLeafNode(this.data) + + // For column nodes, show type as secondary text + if (this.data.nodeType === NodeType.REDSHIFT_COLUMN && this.data.value?.type) { + return createColumnTreeItem(label, this.data.value.type, this.data.nodeType) + } + + const collapsibleState = isLeaf + ? vscode.TreeItemCollapsibleState.None + : vscode.TreeItemCollapsibleState.Collapsed + + const item = new vscode.TreeItem(label, collapsibleState) + + // Set icon based on node type + item.iconPath = getIconForNodeType(this.data.nodeType, this.data.isContainer) + + // Set context value for command enablement + item.contextValue = this.data.nodeType + + // Set tooltip + item.tooltip = getTooltip(this.data) + + return item + } + + public getParent(): TreeNode | undefined { + return this.data.parent + } +} + +/** + * Creates a Lakehouse connection node + */ +export function createLakehouseConnectionNode( + connection: DataZoneConnection, + connectionCredentialsProvider: ConnectionCredentialsProvider, + region: string +): LakehouseNode { + const logger = getLogger('smus') + + // Create Glue clients + const clientStore = ConnectionClientStore.getInstance() + const glueCatalogClient = clientStore.getGlueCatalogClient( + connection.connectionId, + region, + connectionCredentialsProvider + ) + const glueClient = clientStore.getGlueClient(connection.connectionId, region, connectionCredentialsProvider) + + // Create the connection node + return new LakehouseNode( + { + id: connection.connectionId, + nodeType: NodeType.CONNECTION, + value: { connection }, + path: { + connection: connection.name, + }, + }, + async (node) => { + return telemetry.smus_renderLakehouseNode.run(async (span) => { + await recordDataConnectionTelemetry(span, connection, connectionCredentialsProvider) + try { + logger.info(`Loading Lakehouse catalogs for connection ${connection.name}`) + + // Check if this is a default connection + const isDefaultConnection = + DATA_DEFAULT_IAM_CONNECTION_NAME_REGEXP.test(connection.name) || + DATA_DEFAULT_LAKEHOUSE_CONNECTION_NAME_REGEXP.test(connection.name) || + DATA_DEFAULT_ATHENA_CONNECTION_NAME_REGEXP.test(connection.name) + + // Follow the reference pattern with Promise.allSettled + const [awsDataCatalogResult, catalogsResult] = await Promise.allSettled([ + // AWS Data Catalog node (only for default connections) + isDefaultConnection + ? Promise.resolve([createAwsDataCatalogNode(node, glueClient)]) + : Promise.resolve([]), + // Get catalogs by calling Glue API + getCatalogs(glueCatalogClient, glueClient, node), + ]) + + const awsDataCatalog = awsDataCatalogResult.status === 'fulfilled' ? awsDataCatalogResult.value : [] + const apiCatalogs = catalogsResult.status === 'fulfilled' ? catalogsResult.value : [] + const errors: LakehouseNode[] = [] + + if (awsDataCatalogResult.status === 'rejected') { + const error = awsDataCatalogResult.reason as Error + const errorMessage = error.message + errors.push(createErrorItem(errorMessage, 'aws-data-catalog', node.id) as LakehouseNode) + await handleCredExpiredError(error, true) + } + + if (catalogsResult.status === 'rejected') { + const error = catalogsResult.reason as Error + const errorMessage = error.message + errors.push(createErrorItem(errorMessage, 'catalogs', node.id) as LakehouseNode) + await handleCredExpiredError(error, true) + } + + const allNodes = [...awsDataCatalog, ...apiCatalogs, ...errors] + return allNodes.length > 0 + ? allNodes + : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode] + } catch (err) { + logger.error(`Failed to get Lakehouse catalogs: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'lakehouse-catalogs', node.id) as LakehouseNode] + } + }) + } + ) +} + +/** + * Creates AWS Data Catalog node for default connections + */ +function createAwsDataCatalogNode(parent: LakehouseNode, glueClient: GlueClient): LakehouseNode { + return new LakehouseNode( + { + id: `${parent.id}${NODE_ID_DELIMITER}${AWS_DATA_CATALOG}`, + nodeType: NodeType.GLUE_CATALOG, + value: { + catalog: { name: AWS_DATA_CATALOG, type: 'AWS' }, + catalogName: AWS_DATA_CATALOG, + }, + path: { + ...parent.data.path, + catalog: AWS_DATA_CATALOG, + }, + parent, + }, + async (node) => { + const allDatabases = [] + let nextToken: string | undefined + + do { + const { databases, nextToken: token } = await glueClient.getDatabases( + undefined, + 'ALL', + ['NAME'], + nextToken + ) + allDatabases.push(...databases) + nextToken = token + } while (nextToken) + + return allDatabases.length > 0 + ? allDatabases.map((database) => createDatabaseNode(database.Name || '', database, glueClient, node)) + : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode] + } + ) +} + +export interface CatalogTree { + parent: Catalog + children?: Catalog[] +} + +/** + * Builds catalog tree from flat catalog list + * + * AWS Glue catalogs can have parent-child relationships, but the API returns them as a flat list. + * This function reconstructs the hierarchical tree structure needed for proper UI display. + * + * Two-pass algorithm is required because: + * 1. First pass: Create a lookup map of all catalogs by name for O(1) access during relationship building + * 2. Second pass: Build parent-child relationships by linking catalogs that reference ParentCatalogNames + * + * Without the first pass, we'd need O(n²) time to find parent catalogs for each child catalog. + */ +function buildCatalogTree(catalogs: Catalog[]): CatalogTree[] { + const catalogMap: Record = {} + const rootCatalogs: CatalogTree[] = [] + + // First pass: create a map of all catalogs with their metadata + // This allows us to quickly look up any catalog by name when building parent-child relationships in the second pass + for (const catalog of catalogs) { + if (catalog.Name) { + catalogMap[catalog.Name] = { parent: catalog, children: [] } + } + } + + // Second pass: build the hierarchical tree structure by linking children to their parents + // Catalogs with ParentCatalogNames become children, others become root-level catalogs + for (const catalog of catalogs) { + if (catalog.Name) { + if (catalog.ParentCatalogNames && catalog.ParentCatalogNames.length > 0) { + const parentName = catalog.ParentCatalogNames[0] + const parent = catalogMap[parentName] + if (parent) { + if (!parent.children) { + parent.children = [] + } + parent.children.push(catalog) + } + } else { + rootCatalogs.push(catalogMap[catalog.Name]) + } + } + } + rootCatalogs.sort((a, b) => { + const timeA = new Date(a.parent.CreateTime ?? 0).getTime() + const timeB = new Date(b.parent.CreateTime ?? 0).getTime() + return timeA - timeB // For oldest first + }) + + return rootCatalogs +} + +/** + * Gets catalogs from the GlueCatalogClient + */ +async function getCatalogs( + glueCatalogClient: GlueCatalogClient, + glueClient: GlueClient, + parent: LakehouseNode +): Promise { + const allCatalogs = [] + let nextToken: string | undefined + + do { + const { catalogs, nextToken: token } = await glueCatalogClient.getCatalogs(nextToken) + allCatalogs.push(...catalogs) + nextToken = token + } while (nextToken) + + const catalogs = allCatalogs + const tree = buildCatalogTree(catalogs) + + return tree.map((catalog) => { + const parentCatalog = catalog.parent + + // If parent catalog has children, create node that shows child catalogs + if (catalog.children && catalog.children.length > 0) { + return new LakehouseNode( + { + id: parentCatalog.Name || parentCatalog.CatalogId || '', + nodeType: NodeType.GLUE_CATALOG, + value: { + catalog: parentCatalog, + catalogName: parentCatalog.Name || '', + }, + path: { + ...parent.data.path, + catalog: parentCatalog.CatalogId || '', + }, + parent, + }, + async (node: LakehouseNode) => { + // Parent catalogs only show child catalogs + const childCatalogs = + catalog.children?.map((childCatalog) => + createCatalogNode(childCatalog.CatalogId || '', childCatalog, glueClient, node, false) + ) || [] + return childCatalogs + } + ) + } + + // For catalogs without children, create regular catalog node + // For RedLake and S3TableCatalog, they are supposed to have children + // Pass isParent as true + return createCatalogNode( + parentCatalog.CatalogId || '', + parentCatalog, + glueClient, + parent, + isRedLakeCatalog(parentCatalog) || isS3TablesCatalog(parentCatalog) + ) + }) +} + +/** + * Creates a catalog node + */ +function createCatalogNode( + catalogId: string, + catalog: Catalog, + glueClient: GlueClient, + parent: LakehouseNode, + isParent: boolean = false +): LakehouseNode { + const logger = getLogger('smus') + + return new LakehouseNode( + { + id: catalog.Name || catalogId, + nodeType: NodeType.GLUE_CATALOG, + value: { + catalog, + catalogName: catalog.Name || catalogId, + }, + path: { + ...parent.data.path, + catalog: catalogId, + }, + parent, + }, + // Child catalogs load databases, parent catalogs will have their children provider overridden + isParent + ? async () => [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode] + : async (node) => { + try { + logger.info(`Loading databases for catalog ${catalogId}`) + + const allDatabases = [] + let nextToken: string | undefined + + do { + const { databases, nextToken: token } = await glueClient.getDatabases( + catalogId, + undefined, + ['NAME'], + nextToken + ) + allDatabases.push(...databases) + nextToken = token + } while (nextToken) + + return allDatabases.length > 0 + ? allDatabases.map((database) => + createDatabaseNode(database.Name || '', database, glueClient, node) + ) + : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode] + } catch (err) { + logger.error(`Failed to get databases for catalog ${catalogId}: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'catalog-databases', node.id) as LakehouseNode] + } + } + ) +} + +/** + * Creates a database node + */ +function createDatabaseNode( + databaseName: string, + database: Database, + glueClient: GlueClient, + parent: LakehouseNode +): LakehouseNode { + const logger = getLogger('smus') + + return new LakehouseNode( + { + id: databaseName, + nodeType: NodeType.GLUE_DATABASE, + value: { + database, + databaseName, + }, + path: { + ...parent.data.path, + database: databaseName, + }, + parent, + }, + async (node) => { + try { + logger.info(`Loading tables for database ${databaseName}`) + + const allTables = [] + let nextToken: string | undefined + const catalogId = parent.data.path?.catalog === AWS_DATA_CATALOG ? undefined : parent.data.path?.catalog + + do { + const { tables, nextToken: token } = await glueClient.getTables( + databaseName, + catalogId, + ['NAME', 'TABLE_TYPE'], + nextToken + ) + allTables.push(...tables) + nextToken = token + } while (nextToken) + + // Group tables and views separately + const tables = allTables.filter((table) => table.TableType !== DatabaseObjects.VIRTUAL_VIEW) + const views = allTables.filter((table) => table.TableType === DatabaseObjects.VIRTUAL_VIEW) + + const containerNodes: LakehouseNode[] = [] + + // Create tables container if there are tables + if (tables.length > 0) { + containerNodes.push(createContainerNode(NodeType.GLUE_TABLE, tables, glueClient, node)) + } + + // Create views container if there are views + if (views.length > 0) { + containerNodes.push(createContainerNode(NodeType.GLUE_VIEW, views, glueClient, node)) + } + + return containerNodes.length > 0 + ? containerNodes + : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode] + } catch (err) { + logger.error(`Failed to get tables for database ${databaseName}: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'database-tables', node.id) as LakehouseNode] + } + } + ) +} + +/** + * Creates a table node + */ +function createTableNode( + tableName: string, + table: Table, + glueClient: GlueClient, + parent: LakehouseNode +): LakehouseNode { + const logger = getLogger('smus') + + return new LakehouseNode( + { + id: tableName, + nodeType: NodeType.GLUE_TABLE, + value: { + table, + tableName, + }, + path: { + ...parent.data.path, + table: tableName, + }, + parent, + }, + async (node) => { + try { + logger.info(`Loading columns for table ${tableName}`) + + const databaseName = node.data.path?.database || '' + const catalogId = node.data.path?.catalog === AWS_DATA_CATALOG ? undefined : node.data.path?.catalog + const tableDetails = await glueClient.getTable(databaseName, tableName, catalogId) + const columns = tableDetails?.StorageDescriptor?.Columns || [] + const partitions = tableDetails?.PartitionKeys || [] + + const allColumns = [...columns, ...partitions] + return allColumns.length > 0 + ? allColumns.map((column) => createColumnNode(column.Name || '', column, node)) + : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode] + } catch (err) { + logger.error(`Failed to get columns for table ${tableName}: ${(err as Error).message}`) + await handleCredExpiredError(err) + return [] + } + } + ) +} + +/** + * Creates a column node + */ +function createColumnNode(columnName: string, column: Column, parent: LakehouseNode): LakehouseNode { + const columnType = getColumnType(column?.Type) + + return new LakehouseNode({ + id: `${parent.id}${NODE_ID_DELIMITER}${columnName}`, + nodeType: NodeType.REDSHIFT_COLUMN, + value: { + name: columnName, + type: columnType, + }, + path: { + ...parent.data.path, + column: columnName, + }, + parent, + }) +} + +/** + * Creates a container node for grouping objects by type + */ +function createContainerNode( + nodeType: NodeType, + items: Table[], + glueClient: GlueClient, + parent: LakehouseNode +): LakehouseNode { + return new LakehouseNode( + { + id: `${parent.id}${NODE_ID_DELIMITER}${nodeType}-container`, + nodeType: nodeType, + value: { + items, + }, + path: parent.data.path, + parent, + isContainer: true, + }, + async (node) => { + // Map items to nodes + return items.length > 0 + ? items.map((item) => createTableNode(item.Name || '', item, glueClient, node)) + : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as LakehouseNode] + } + ) +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/redshiftStrategy.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/redshiftStrategy.ts new file mode 100644 index 00000000000..45d82c44ecd --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/redshiftStrategy.ts @@ -0,0 +1,1041 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { getLogger } from '../../../shared/logger/logger' +import { DataZoneConnection } from '../../shared/client/datazoneClient' +import { ConnectionConfig, createRedshiftConnectionConfig } from '../../shared/client/sqlWorkbenchClient' +import { ConnectionClientStore } from '../../shared/client/connectionClientStore' +import { NODE_ID_DELIMITER, NodeType, ResourceType, NodeData, NO_DATA_FOUND_MESSAGE } from './types' +import { + getLabel, + isLeafNode, + getIconForNodeType, + createColumnTreeItem, + isRedLakeDatabase, + getTooltip, + getColumnType, + createErrorItem, +} from './utils' +import { createPlaceholderItem } from '../../../shared/treeview/utils' +import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider' +import { Catalog } from '@amzn/glue-catalog-client' +import { handleCredExpiredError } from '../../shared/credentialExpiryHandler' +import { telemetry } from '../../../shared/telemetry/telemetry' +import { recordDataConnectionTelemetry } from '../../shared/telemetry' + +/** + * Redshift data node for SageMaker Unified Studio + */ +export class RedshiftNode implements TreeNode { + private childrenNodes: TreeNode[] | undefined + private isLoading = false + private readonly logger = getLogger('smus') + + constructor( + public readonly data: NodeData, + private readonly childrenProvider?: (node: RedshiftNode) => Promise + ) {} + + public get id(): string { + return this.data.id + } + + public get resource(): any { + return this.data.value || {} + } + + public async getChildren(): Promise { + // Return cached children if available + if (this.childrenNodes && !this.isLoading) { + return this.childrenNodes + } + + // Return empty array for leaf nodes + if (isLeafNode(this.data)) { + return [] + } + + // If we have a children provider, use it + if (this.childrenProvider) { + try { + this.isLoading = true + const childrenNodes = await this.childrenProvider(this) + this.childrenNodes = childrenNodes + this.isLoading = false + return this.childrenNodes + } catch (err) { + this.isLoading = false + this.logger.error(`Failed to get children for node ${this.data.id}: ${(err as Error).message}`) + + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'getChildren', this.id) as RedshiftNode] + } + } + + return [] + } + + public async getTreeItem(): Promise { + const label = getLabel(this.data) + const isLeaf = isLeafNode(this.data) + + // For column nodes, create a TreeItem with label and description (column type) + if (this.data.nodeType === NodeType.REDSHIFT_COLUMN && this.data.value?.type) { + return createColumnTreeItem(label, this.data.value.type, this.data.nodeType) + } + + // For other nodes, use standard TreeItem + const collapsibleState = isLeaf + ? vscode.TreeItemCollapsibleState.None + : vscode.TreeItemCollapsibleState.Collapsed + + const item = new vscode.TreeItem(label, collapsibleState) + + // Set icon based on node type + item.iconPath = getIconForNodeType(this.data.nodeType, this.data.isContainer) + + // Set context value for command enablement + item.contextValue = this.data.nodeType + + // Set tooltip + item.tooltip = getTooltip(this.data) + + return item + } + + public getParent(): TreeNode | undefined { + return this.data.parent + } +} + +/** + * Creates a Redshift connection node + */ +export function createRedshiftConnectionNode( + connection: DataZoneConnection, + connectionCredentialsProvider: ConnectionCredentialsProvider +): RedshiftNode { + const logger = getLogger('smus') + return new RedshiftNode( + { + id: connection.connectionId, + nodeType: NodeType.CONNECTION, + value: { connection, connectionCredentialsProvider }, + path: { + connection: connection.name, + }, + }, + async (node) => { + return telemetry.smus_renderRedshiftNode.run(async (span) => { + logger.info(`Loading Redshift resources for connection ${connection.name}`) + await recordDataConnectionTelemetry(span, connection, connectionCredentialsProvider) + + const connectionParams = extractConnectionParams(connection) + if (!connectionParams) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode] + } + + const isGlueCatalogDatabase = isRedLakeDatabase(connectionParams.database) + + // Create connection config with all available information + const connectionConfig = await createRedshiftConnectionConfig( + connectionParams.host, + connectionParams.database, + connectionParams.accountId, + connectionParams.region, + connectionParams.secretArn, + isGlueCatalogDatabase + ) + + // Wake up the database with a simple query + await wakeUpDatabase( + connectionConfig, + connectionParams.region, + connectionCredentialsProvider, + connection + ) + + const clientStore = ConnectionClientStore.getInstance() + const sqlClient = clientStore.getSQLWorkbenchClient( + connection.connectionId, + connectionParams.region, + connectionCredentialsProvider + ) + + // Fetch Glue catalogs for filtering purposes only + // This will help determine which catalogs are accessible within the project + let glueCatalogs: Catalog[] = [] + try { + glueCatalogs = await listGlueCatalogs( + connection.connectionId, + connectionParams.region, + connectionCredentialsProvider + ) + } catch (err) { + logger.warn(`Failed to fetch Glue catalogs for filtering: ${(err as Error).message}`) + } + + // Fetch databases and catalogs using getResources + const [databasesResult, catalogsResult] = await Promise.allSettled([ + fetchResources(sqlClient, connectionConfig, ResourceType.DATABASE), + fetchResources(sqlClient, connectionConfig, ResourceType.CATALOG), + ]) + + const databases = databasesResult.status === 'fulfilled' ? databasesResult.value : [] + const catalogs = catalogsResult.status === 'fulfilled' ? catalogsResult.value : [] + const allNodes: RedshiftNode[] = [] + + // Filter databases + const filteredDatabases = databases.filter( + (r: any) => + r.type === ResourceType.DATABASE || + r.type === ResourceType.EXTERNAL_DATABASE || + r.type === ResourceType.SHARED_DATABASE + ) + + // Filter catalogs using listGlueCatalogs results + const filteredCatalogs = catalogs.filter((catalog: any) => { + if (catalog.displayName?.toLowerCase() === 'awsdatacatalog') { + return true // Always include AWS Data Catalog + } + // Filter using Glue catalogs list + return glueCatalogs.some((glueCatalog) => catalog.displayName?.endsWith(glueCatalog.Name ?? '')) + }) + + // Add database nodes + if (filteredDatabases.length === 0) { + if (databasesResult.status === 'rejected') { + const error = databasesResult.reason as Error + const errorMessage = `Failed to fetch databases - ${databasesResult.reason?.message || databasesResult.reason}.` + allNodes.push(createErrorItem(errorMessage, 'databases', node.id) as RedshiftNode) + await handleCredExpiredError(error, true) + } else { + allNodes.push(createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode) + } + } else { + allNodes.push( + ...filteredDatabases.map((db: any) => + createDatabaseNode(db.displayName, connectionConfig, node) + ) + ) + } + + // Add catalog nodes + if (filteredCatalogs.length === 0) { + if (catalogsResult.status === 'rejected') { + const error = catalogsResult.reason as Error + const errorMessage = `Failed to fetch catalogs - ${catalogsResult.reason?.message || catalogsResult.reason}` + allNodes.push(createErrorItem(errorMessage, 'catalogs', node.id) as RedshiftNode) + await handleCredExpiredError(error, true) + } else { + allNodes.push(createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode) + } + } else { + allNodes.push( + ...filteredCatalogs.map((catalog: any) => + createCatalogNode( + catalog.displayName || catalog.identifier || '', + catalog, + connectionConfig, + node + ) + ) + ) + } + + return allNodes + }) + } + ) +} + +/** + * Extracts connection parameters from DataZone connection + */ +function extractConnectionParams(connection: DataZoneConnection) { + const redshiftProps = connection.props?.redshiftProperties || {} + const jdbcConnection = connection.props?.jdbcConnection || {} + + let host = jdbcConnection.host + if (!host && jdbcConnection.jdbcUrl) { + // Example: jdbc:redshift://test-cluster.123456789012.us-east-1.redshift.amazonaws.com:5439/dev + // match[0] = entire URL, match[1] = host, match[2] = port, match[3] = database + const match = jdbcConnection.jdbcUrl.match(/jdbc:redshift:\/\/([^:]+):(\d+)\/(.+)/) + if (match) { + host = match[1] + } + } + + const database = jdbcConnection.dbname || redshiftProps.databaseName + const secretArn = jdbcConnection.secretId || redshiftProps.credentials?.secretArn + const accountId = connection.location?.awsAccountId + const region = connection.location?.awsRegion + + if (!host || !database || !accountId || !region) { + return undefined + } + + return { host, database, secretArn, accountId, region } +} + +/** + * Wake up the database with a simple query + */ +async function wakeUpDatabase( + connectionConfig: ConnectionConfig, + region: string, + connectionCredentialsProvider: ConnectionCredentialsProvider, + connection: DataZoneConnection +) { + const logger = getLogger('smus') + const clientStore = ConnectionClientStore.getInstance() + const sqlClient = clientStore.getSQLWorkbenchClient(connection.connectionId, region, connectionCredentialsProvider) + try { + await sqlClient.executeQuery(connectionConfig, 'select 1 from sys_query_history limit 1;') + } catch (e) { + logger.debug(`Wake-up query failed: ${(e as Error).message}`) + } +} + +/** + * Creates a database node + */ +function createDatabaseNode( + databaseName: string, + connectionConfig: ConnectionConfig, + parent: RedshiftNode +): RedshiftNode { + const logger = getLogger('smus') + + return new RedshiftNode( + { + id: databaseName, + nodeType: NodeType.REDSHIFT_DATABASE, + value: { + database: databaseName, + connectionConfig, + identifier: databaseName, + type: ResourceType.DATABASE, + childObjectTypes: [ResourceType.SCHEMA, ResourceType.EXTERNAL_SCHEMA, ResourceType.SHARED_SCHEMA], + }, + path: { + ...parent.data.path, + database: databaseName, + }, + parent, + }, + async (node) => { + try { + // Get the original credentials from the root connection node + const rootCredentials = getRootCredentials(parent) + + // Create SQL client with the original credentials + const clientStore = ConnectionClientStore.getInstance() + const sqlClient = clientStore.getSQLWorkbenchClient( + connectionConfig.id, + connectionConfig.id.split(':')[3], // region + rootCredentials + ) + + // Update connection config with the database + const dbConnectionConfig = { + ...connectionConfig, + database: databaseName, + } + + // Get schemas + const allResources = [] + let nextToken: string | undefined + + do { + const response = await sqlClient.getResources({ + connection: dbConnectionConfig, + resourceType: ResourceType.SCHEMA, + includeChildren: true, + maxItems: 100, + parents: [ + { + parentId: databaseName, + parentType: ResourceType.DATABASE, + }, + ], + forceRefresh: true, + pageToken: nextToken, + }) + allResources.push(...(response.resources || [])) + nextToken = response.nextToken + } while (nextToken) + + const schemas = allResources.filter( + (r: any) => + r.type === ResourceType.SCHEMA || + r.type === ResourceType.EXTERNAL_SCHEMA || + r.type === ResourceType.SHARED_SCHEMA + ) + + if (schemas.length === 0) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode] + } + + // Map schemas to nodes + return schemas.map((schema: any) => createSchemaNode(schema.displayName, dbConnectionConfig, node)) + } catch (err) { + logger.error(`Failed to get schemas: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'schemas', node.id) as RedshiftNode] + } + } + ) +} + +/** + * Creates a schema node + */ +function createSchemaNode(schemaName: string, connectionConfig: ConnectionConfig, parent: RedshiftNode): RedshiftNode { + const logger = getLogger('smus') + + return new RedshiftNode( + { + id: schemaName, + nodeType: NodeType.REDSHIFT_SCHEMA, + value: { + schema: schemaName, + connectionConfig, + identifier: schemaName, + type: ResourceType.SCHEMA, + childObjectTypes: [ + ResourceType.TABLE, + ResourceType.VIEW, + ResourceType.FUNCTION, + ResourceType.STORED_PROCEDURE, + ResourceType.EXTERNAL_TABLE, + ResourceType.CATALOG_TABLE, + ResourceType.DATA_CATALOG_TABLE, + ], + }, + path: { + ...parent.data.path, + schema: schemaName, + }, + parent, + }, + async (node) => { + try { + // Get the original credentials from the root connection node + const rootCredentials = getRootCredentials(parent) + + // Create SQL client with the original credentials + const clientStore = ConnectionClientStore.getInstance() + const rootConnection = getRootConnection(parent) + const sqlClient = clientStore.getSQLWorkbenchClient( + rootConnection.connectionId, + connectionConfig.id.split(':')[3], // region + rootCredentials + ) + + // Get schema objects + // Make sure we're using the correct database in the connection config + const schemaConnectionConfig = { + ...connectionConfig, + database: parent.data.path?.database || connectionConfig.database, + } + + // Create request params object for logging + const requestParams = { + connection: schemaConnectionConfig, + resourceType: ResourceType.TABLE, + includeChildren: true, + maxItems: 100, + parents: [ + { + parentId: schemaName, + parentType: ResourceType.SCHEMA, + }, + { + parentId: schemaConnectionConfig.database, + parentType: ResourceType.DATABASE, + }, + ], + forceRefresh: true, + } + + const allResources = [] + let nextToken: string | undefined + + do { + const response = await sqlClient.getResources({ + ...requestParams, + pageToken: nextToken, + }) + allResources.push(...(response.resources || [])) + nextToken = response.nextToken + } while (nextToken) + + // Group resources by type + const tables = allResources.filter( + (r: any) => + r.type === ResourceType.TABLE || + r.type === ResourceType.EXTERNAL_TABLE || + r.type === ResourceType.CATALOG_TABLE || + r.type === ResourceType.DATA_CATALOG_TABLE + ) + const views = allResources.filter((r: any) => r.type === ResourceType.VIEW) + const functions = allResources.filter((r: any) => r.type === ResourceType.FUNCTION) + const procedures = allResources.filter((r: any) => r.type === ResourceType.STORED_PROCEDURE) + + // Create container nodes for each type + const containerNodes: RedshiftNode[] = [] + + // Tables container + if (tables.length > 0) { + containerNodes.push(createContainerNode(NodeType.REDSHIFT_TABLE, tables, connectionConfig, node)) + } + + // Views container + if (views.length > 0) { + containerNodes.push(createContainerNode(NodeType.REDSHIFT_VIEW, views, connectionConfig, node)) + } + + // Functions container + if (functions.length > 0) { + containerNodes.push( + createContainerNode(NodeType.REDSHIFT_FUNCTION, functions, connectionConfig, node) + ) + } + + // Stored procedures container + if (procedures.length > 0) { + containerNodes.push( + createContainerNode(NodeType.REDSHIFT_STORED_PROCEDURE, procedures, connectionConfig, node) + ) + } + + if (containerNodes.length === 0) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode] + } + + return containerNodes + } catch (err) { + logger.error(`Failed to get schema contents: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'schema-contents', node.id) as RedshiftNode] + } + } + ) +} + +/** + * Creates a container node for grouping objects by type + */ +function createContainerNode( + nodeType: NodeType, + resources: any[], + connectionConfig: ConnectionConfig, + parent: RedshiftNode +): RedshiftNode { + return new RedshiftNode( + { + id: `${parent.id}${NODE_ID_DELIMITER}${nodeType}-container`, + nodeType: nodeType, + value: { + connectionConfig, + resources, + }, + path: parent.data.path, + parent, + isContainer: true, + }, + async (node) => { + // Map resources to nodes + if (nodeType === NodeType.REDSHIFT_TABLE && parent.data.value?.type === ResourceType.CATALOG_DATABASE) { + // For catalog tables, use catalog table node + return resources.length > 0 + ? resources.map((resource: any) => + createCatalogTableNode(resource.displayName, resource, connectionConfig, node) + ) + : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode] + } + return resources.length > 0 + ? resources.map((resource: any) => + createObjectNode(resource.displayName, nodeType, resource, connectionConfig, node) + ) + : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode] + } + ) +} + +/** + * Creates an object node (table, view, function, etc.) + */ +function createObjectNode( + name: string, + nodeType: NodeType, + resource: any, + connectionConfig: ConnectionConfig, + parent: RedshiftNode +): RedshiftNode { + const logger = getLogger('smus') + + return new RedshiftNode( + { + id: `${parent.id}${NODE_ID_DELIMITER}${name}`, + nodeType: nodeType, + value: { + ...resource, + connectionConfig, + }, + path: { + ...parent.data.path, + [nodeType]: name, + }, + parent, + }, + async (node) => { + // Only tables have columns + if (nodeType !== NodeType.REDSHIFT_TABLE) { + return [] + } + + try { + // Get the original credentials from the root connection node + const rootCredentials = getRootCredentials(parent) + + // Create SQL client with the original credentials + const clientStore = ConnectionClientStore.getInstance() + const rootConnection = getRootConnection(parent) + const sqlClient = clientStore.getSQLWorkbenchClient( + rootConnection.connectionId, + connectionConfig.id.split(':')[3], // region + rootCredentials + ) + + // Get schema and database from path + const schemaName = node.data.path?.schema + const databaseName = node.data.path?.database + const tableName = node.data.path?.table + + if (!schemaName || !databaseName || !tableName) { + logger.error('Missing schema, database, or table name in path') + return [] + } + + // Create request params for getResources to get columns + const requestParams = { + connection: connectionConfig, + resourceType: ResourceType.COLUMNS, + includeChildren: true, + maxItems: 100, + parents: [ + { + parentId: tableName, + parentType: ResourceType.TABLE, + }, + { + parentId: schemaName, + parentType: ResourceType.SCHEMA, + }, + { + parentId: databaseName, + parentType: ResourceType.DATABASE, + }, + ], + forceRefresh: true, + } + + // Call getResources to get columns + const allColumns = [] + let nextToken: string | undefined + + do { + const response = await sqlClient.getResources({ + ...requestParams, + pageToken: nextToken, + }) + allColumns.push(...(response.resources || [])) + nextToken = response.nextToken + } while (nextToken) + + // Create column nodes from API response + return allColumns.length > 0 + ? allColumns.map((column: any) => { + // Extract column type from resourceMetadata + let columnType = 'UNKNOWN' + if (column.resourceMetadata && Array.isArray(column.resourceMetadata)) { + const typeMetadata = column.resourceMetadata.find( + (meta: any) => meta.key === 'COLUMN_TYPE' + ) + if (typeMetadata) { + columnType = typeMetadata.value + } + } + + columnType = getColumnType(columnType) + + return createColumnNode( + column.displayName, + { + name: column.displayName, + type: columnType, + }, + connectionConfig, + node + ) + }) + : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode] + } catch (err) { + logger.error(`Failed to get columns: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'columns', node.id) as RedshiftNode] + } + } + ) +} + +/** + * Creates a column node + */ +function createColumnNode( + name: string, + columnInfo: { name: string; type: string }, + connectionConfig: ConnectionConfig, + parent: RedshiftNode +): RedshiftNode { + return new RedshiftNode({ + id: `${parent.id}${NODE_ID_DELIMITER}${name}`, + nodeType: NodeType.REDSHIFT_COLUMN, + value: { + name, + type: columnInfo.type, + connectionConfig, + }, + path: { + ...parent.data.path, + column: name, + }, + parent, + }) +} + +/** + * Gets the root connection from a node + */ +function getRootConnection(node: RedshiftNode): DataZoneConnection { + // Start with the current node + let currentNode = node + + // Traverse up to the root connection node + while (currentNode.data.parent) { + currentNode = currentNode.data.parent + } + + // Get connection from the root node + return currentNode.data.value?.connection +} + +/** + * Gets the original credentials from the root connection node + */ +function getRootCredentials(node: RedshiftNode): ConnectionCredentialsProvider { + // Start with the current node + let currentNode = node + + // Traverse up to the root connection node + while (currentNode.data.parent) { + currentNode = currentNode.data.parent + } + + // Get credentials from the root node + const credentials = currentNode.data.value?.connectionCredentialsProvider + + // Return credentials or fallback to dummy credentials + return ( + credentials || { + accessKeyId: 'dummy', + secretAccessKey: 'dummy', + } + ) +} + +/** + * Fetch glue catalogs, this will help determine which catalogs are accessible within the project + */ +async function listGlueCatalogs( + connectionId: string, + region: string, + connectionCredentialsProvider: ConnectionCredentialsProvider +): Promise { + const clientStore = ConnectionClientStore.getInstance() + const glueCatalogClient = clientStore.getGlueCatalogClient(connectionId, region, connectionCredentialsProvider) + + const allCatalogs = [] + let nextToken: string | undefined + + do { + const { catalogs, nextToken: token } = await glueCatalogClient.getCatalogs(nextToken) + allCatalogs.push(...catalogs) + nextToken = token + } while (nextToken) + + return allCatalogs +} + +/** + * Main logic to fetch catalog and database resources using getResources + */ +async function fetchResources( + sqlClient: any, + connectionConfig: ConnectionConfig, + resourceType: ResourceType, + parents: any[] = [] +): Promise { + const allResources = [] + let nextToken: string | undefined + + do { + const requestParams = { + connection: connectionConfig, + resourceType, + includeChildren: true, + maxItems: 100, + parents, + forceRefresh: true, + pageToken: nextToken, + } + const response = await sqlClient.getResources(requestParams) + allResources.push(...(response.resources || [])) + nextToken = response.nextToken + } while (nextToken) + + return allResources +} + +/** + * Creates a catalog database node + */ +function createCatalogDatabaseNode( + databaseName: string, + database: any, + connectionConfig: ConnectionConfig, + parent: RedshiftNode +): RedshiftNode { + return new RedshiftNode( + { + id: `${parent.id}${NODE_ID_DELIMITER}${databaseName}`, + nodeType: NodeType.REDSHIFT_CATALOG_DATABASE, + value: { + ...database, + connectionConfig, + identifier: databaseName, + type: ResourceType.CATALOG_DATABASE, + }, + path: { + ...parent.data.path, + database: databaseName, + }, + parent, + }, + async (node) => { + try { + const rootCredentials = getRootCredentials(parent) + const clientStore = ConnectionClientStore.getInstance() + const rootConnection = getRootConnection(parent) + const sqlClient = clientStore.getSQLWorkbenchClient( + rootConnection.connectionId, + connectionConfig.id.split(':')[3], + rootCredentials + ) + + // Use getResources to fetch tables within this catalog database + const tables = await fetchResources(sqlClient, connectionConfig, ResourceType.CATALOG_TABLE, [ + { + parentId: database.identifier, + parentType: ResourceType.CATALOG_DATABASE, + }, + { + parentId: parent.data.value?.catalog?.identifier || parent.data.path?.catalog, + parentType: ResourceType.CATALOG, + }, + ]) + + if (tables.length === 0) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode] + } + + // Create container node for tables + return [createContainerNode(NodeType.REDSHIFT_TABLE, tables, connectionConfig, node)] + } catch (err) { + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'catalog-tables', node.id) as RedshiftNode] + } + } + ) +} + +/** + * Creates a catalog table node + */ +function createCatalogTableNode( + tableName: string, + table: any, + connectionConfig: ConnectionConfig, + parent: RedshiftNode +): RedshiftNode { + return new RedshiftNode( + { + id: `${parent.id}${NODE_ID_DELIMITER}${tableName}`, + nodeType: NodeType.REDSHIFT_TABLE, + value: { + ...table, + connectionConfig, + }, + path: { + ...parent.data.path, + table: tableName, + }, + parent, + }, + async (node) => { + try { + const rootCredentials = getRootCredentials(parent) + const clientStore = ConnectionClientStore.getInstance() + const rootConnection = getRootConnection(parent) + const sqlClient = clientStore.getSQLWorkbenchClient( + rootConnection.connectionId, + connectionConfig.id.split(':')[3], + rootCredentials + ) + + // Use getResources to fetch columns within this catalog table + // Need to traverse up to find the actual database and catalog nodes + let databaseNode = parent + while (databaseNode && databaseNode.data.nodeType !== NodeType.REDSHIFT_CATALOG_DATABASE) { + databaseNode = databaseNode.data.parent + } + + let catalogNode = databaseNode?.data.parent + while (catalogNode && catalogNode.data.nodeType !== NodeType.REDSHIFT_CATALOG) { + catalogNode = catalogNode.data.parent + } + + const parents = [ + { + parentId: table.identifier, + parentType: ResourceType.CATALOG_TABLE, + }, + { + parentId: databaseNode?.data.value?.identifier, + parentType: ResourceType.CATALOG_DATABASE, + }, + { + parentId: catalogNode?.data.value?.catalog?.identifier || catalogNode?.data.value?.identifier, + parentType: ResourceType.CATALOG, + }, + ] + + const columns = await fetchResources(sqlClient, connectionConfig, ResourceType.CATALOG_COLUMN, parents) + + return columns.length > 0 + ? columns.map((column: any) => { + let columnType = 'UNKNOWN' + if (column.resourceMetadata && Array.isArray(column.resourceMetadata)) { + const typeMetadata = column.resourceMetadata.find( + (meta: any) => meta.key === 'COLUMN_TYPE' + ) + if (typeMetadata) { + columnType = typeMetadata.value + } + } + + columnType = getColumnType(columnType) + + return createColumnNode( + column.displayName, + { + name: column.displayName, + type: columnType, + }, + connectionConfig, + node + ) + }) + : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode] + } catch (err) { + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'catalog-columns', node.id) as RedshiftNode] + } + } + ) +} + +/** + * Creates a catalog node + */ +function createCatalogNode( + catalogName: string, + catalog: any, + connectionConfig: ConnectionConfig, + parent: RedshiftNode +): RedshiftNode { + return new RedshiftNode( + { + id: `${parent.id}${NODE_ID_DELIMITER}${catalogName}`, + nodeType: NodeType.REDSHIFT_CATALOG, + value: { + catalog, + catalogName, + connectionConfig, + identifier: catalogName, + type: ResourceType.CATALOG, + }, + path: { + ...parent.data.path, + catalog: catalogName, + }, + parent, + }, + async (node) => { + try { + const rootCredentials = getRootCredentials(parent) + const clientStore = ConnectionClientStore.getInstance() + const rootConnection = getRootConnection(parent) + const sqlClient = clientStore.getSQLWorkbenchClient( + rootConnection.connectionId, + connectionConfig.id.split(':')[3], + rootCredentials + ) + + // Use getResources to fetch databases within this catalog + const databases = await fetchResources(sqlClient, connectionConfig, ResourceType.CATALOG_DATABASE, [ + { + parentId: catalog.identifier, + parentType: ResourceType.CATALOG, + }, + ]) + + if (databases.length === 0) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode] + } + + return databases.length > 0 + ? databases.map((database: any) => + createCatalogDatabaseNode(database.displayName, database, connectionConfig, node) + ) + : [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as RedshiftNode] + } catch (err) { + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'catalog-databases', node.id) as RedshiftNode] + } + } + ) +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/s3Strategy.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/s3Strategy.ts new file mode 100644 index 00000000000..53d481bd2de --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/s3Strategy.ts @@ -0,0 +1,698 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { getLogger } from '../../../shared/logger/logger' +import { DataZoneConnection } from '../../shared/client/datazoneClient' +import { S3Client } from '../../shared/client/s3Client' +import { ConnectionClientStore } from '../../shared/client/connectionClientStore' +import { NODE_ID_DELIMITER, NodeType, ConnectionType, NodeData, NO_DATA_FOUND_MESSAGE } from './types' +import { getLabel, isLeafNode, getIconForNodeType, getTooltip, createErrorItem } from './utils' +import { createPlaceholderItem } from '../../../shared/treeview/utils' +import { + ListCallerAccessGrantsCommand, + GetDataAccessCommand, + ListCallerAccessGrantsEntry, +} from '@aws-sdk/client-s3-control' +import { S3, ListObjectsV2Command } from '@aws-sdk/client-s3' +import { handleCredExpiredError } from '../../shared/credentialExpiryHandler' +import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider' +import { telemetry } from '../../../shared/telemetry/telemetry' +import { recordDataConnectionTelemetry } from '../../shared/telemetry' + +// Regex to match default S3 connection names +// eslint-disable-next-line @typescript-eslint/naming-convention +export const DATA_DEFAULT_S3_CONNECTION_NAME_REGEXP = /^(project\.s3_default_folder)|(default\.s3)$/ + +/** + * S3 data node for SageMaker Unified Studio + */ +export class S3Node implements TreeNode { + private readonly logger = getLogger('smus') + private childrenNodes: TreeNode[] | undefined + private isLoading = false + + constructor( + public readonly data: NodeData, + private readonly childrenProvider?: (node: S3Node) => Promise + ) {} + + public get id(): string { + return this.data.id + } + + public get resource(): any { + return this.data.value || {} + } + + public async getChildren(): Promise { + // Return cached children if available + if (this.childrenNodes && !this.isLoading) { + return this.childrenNodes + } + + // Return empty array for leaf nodes + if (isLeafNode(this.data)) { + return [] + } + + // If we have a children provider, use it + if (this.childrenProvider) { + try { + this.isLoading = true + const childrenNodes = await this.childrenProvider(this) + this.childrenNodes = childrenNodes + this.isLoading = false + return this.childrenNodes + } catch (err) { + this.isLoading = false + this.logger.error(`Failed to get children for node ${this.data.id}: ${(err as Error).message}`) + + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'getChildren', this.id) as S3Node] + } + } + + return [] + } + + public async getTreeItem(): Promise { + const collapsibleState = isLeafNode(this.data) + ? vscode.TreeItemCollapsibleState.None + : vscode.TreeItemCollapsibleState.Collapsed + + const label = getLabel(this.data) + const item = new vscode.TreeItem(label, collapsibleState) + + // Set icon based on node type + item.iconPath = getIconForNodeType(this.data.nodeType, this.data.isContainer) + + // Set context value for command enablement + item.contextValue = this.data.nodeType + + // Set tooltip + item.tooltip = getTooltip(this.data) + + return item + } + + public getParent(): TreeNode | undefined { + return this.data.parent + } +} + +/** + * Creates an S3 connection node + */ +export function createS3ConnectionNode( + connection: DataZoneConnection, + connectionCredentialsProvider: ConnectionCredentialsProvider, + region: string +): S3Node { + const logger = getLogger('smus') + + // Parse S3 URI from connection + const s3Info = parseS3Uri(connection) + if (!s3Info) { + logger.warn(`No S3 URI found in connection properties for connection ${connection.name}`) + const errorMessage = 'No S3 URI configured' + void vscode.window.showErrorMessage(errorMessage) + return createErrorItem(errorMessage, 'connection', connection.connectionId) as S3Node + } + + // Handle case where s3Uri is "s3://" (all buckets access) + const isAllBucketsAccess = !s3Info.bucket + + // Get S3 client from store + const clientStore = ConnectionClientStore.getInstance() + const s3Client = clientStore.getS3Client(connection.connectionId, region, connectionCredentialsProvider) + + // Check if this is a default S3 connection + const isDefaultConnection = DATA_DEFAULT_S3_CONNECTION_NAME_REGEXP.test(connection.name) + + // Create the connection node + return new S3Node( + { + id: connection.connectionId, + nodeType: NodeType.CONNECTION, + connectionType: ConnectionType.S3, + value: { connection }, + path: { + connection: connection.name, + bucket: s3Info.bucket, + }, + }, + async (node) => { + return telemetry.smus_renderS3Node.run(async (span) => { + await recordDataConnectionTelemetry(span, connection, connectionCredentialsProvider) + try { + if (isAllBucketsAccess) { + // For all buckets access (s3://), list all accessible buckets + try { + const buckets = await s3Client.listBuckets() + if (buckets.length === 0) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as S3Node] + } + + return buckets.map((bucket) => { + return new S3Node( + { + id: bucket.Name || 'unknown-bucket', + nodeType: NodeType.S3_BUCKET, + connectionType: ConnectionType.S3, + value: { bucket: bucket.Name }, + path: { + connection: connection.name, + bucket: bucket.Name, + }, + parent: node, + }, + async (bucketNode) => { + try { + const allPaths = [] + let nextToken: string | undefined + + do { + const result = await s3Client.listPaths( + bucket.Name || '', + undefined, + nextToken + ) + allPaths.push(...result.paths) + nextToken = result.nextToken + } while (nextToken) + + if (allPaths.length === 0) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as S3Node] + } + + return allPaths.map((path) => { + const nodeId = `${path.bucket}-${path.prefix || 'root'}` + + return new S3Node( + { + id: nodeId, + nodeType: path.isFolder ? NodeType.S3_FOLDER : NodeType.S3_FILE, + connectionType: ConnectionType.S3, + value: path, + path: { + connection: connection.name, + bucket: path.bucket, + key: path.prefix, + label: path.displayName, + }, + parent: bucketNode, + }, + path.isFolder + ? createFolderChildrenProvider(s3Client, path) + : undefined + ) + }) + } catch (err) { + logger.error(`Failed to list bucket contents: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [ + createErrorItem( + errorMessage, + 'bucket-contents-all-access', + bucketNode.id + ) as S3Node, + ] + } + } + ) + }) + } catch (err) { + logger.error(`Failed to list buckets: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'list-buckets', node.id) as S3Node] + } + } else if (isDefaultConnection && s3Info.prefix) { + // For default connections, show the full path as the first node + const fullPath = `${s3Info.bucket}/${s3Info.prefix}` + return [ + new S3Node( + { + id: fullPath, + nodeType: NodeType.S3_BUCKET, + connectionType: ConnectionType.S3, + value: { bucket: s3Info.bucket, prefix: s3Info.prefix }, + path: { + connection: connection.name, + bucket: s3Info.bucket, + key: s3Info.prefix, + label: fullPath, + }, + parent: node, + }, + async (bucketNode) => { + try { + // List objects starting from the prefix + const allPaths = [] + let nextToken: string | undefined + + do { + const result = await s3Client.listPaths( + s3Info.bucket, + s3Info.prefix, + nextToken + ) + allPaths.push(...result.paths) + nextToken = result.nextToken + } while (nextToken) + + if (allPaths.length === 0) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as S3Node] + } + + // Convert paths to nodes + return allPaths.map((path) => { + const nodeId = `${path.bucket}-${path.prefix || 'root'}` + + return new S3Node( + { + id: nodeId, + nodeType: path.isFolder ? NodeType.S3_FOLDER : NodeType.S3_FILE, + connectionType: ConnectionType.S3, + value: path, + path: { + connection: connection.name, + bucket: path.bucket, + key: path.prefix, + label: path.displayName, + }, + parent: bucketNode, + }, + path.isFolder ? createFolderChildrenProvider(s3Client, path) : undefined + ) + }) + } catch (err) { + logger.error(`Failed to list bucket contents: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [ + createErrorItem( + errorMessage, + 'bucket-contents-default', + bucketNode.id + ) as S3Node, + ] + } + } + ), + ] + } else { + // For non-default connections, show bucket as the first node + return [ + new S3Node( + { + id: s3Info.bucket, + nodeType: NodeType.S3_BUCKET, + connectionType: ConnectionType.S3, + value: { bucket: s3Info.bucket }, + path: { + connection: connection.name, + bucket: s3Info.bucket, + }, + parent: node, + }, + async (bucketNode) => { + try { + // List objects in the bucket + const allPaths = [] + let nextToken: string | undefined + + do { + const result = await s3Client.listPaths( + s3Info.bucket, + s3Info.prefix, + nextToken + ) + allPaths.push(...result.paths) + nextToken = result.nextToken + } while (nextToken) + + if (allPaths.length === 0) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as S3Node] + } + + // Convert paths to nodes + return allPaths.map((path) => { + const nodeId = `${path.bucket}-${path.prefix || 'root'}` + + return new S3Node( + { + id: nodeId, + nodeType: path.isFolder ? NodeType.S3_FOLDER : NodeType.S3_FILE, + connectionType: ConnectionType.S3, + value: path, + path: { + connection: connection.name, + bucket: path.bucket, + key: path.prefix, + label: path.displayName, + }, + parent: bucketNode, + }, + path.isFolder ? createFolderChildrenProvider(s3Client, path) : undefined + ) + }) + } catch (err) { + logger.error(`Failed to list bucket contents: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [ + createErrorItem( + errorMessage, + 'bucket-contents-regular', + bucketNode.id + ) as S3Node, + ] + } + } + ), + ] + } + } catch (err) { + logger.error(`Failed to create bucket node: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'bucket-node', node.id) as S3Node] + } + }) + } + ) +} + +/** + * Creates S3 access grant nodes for project.s3_default_folder connections + */ +export async function createS3AccessGrantNodes( + connection: DataZoneConnection, + connectionCredentialsProvider: ConnectionCredentialsProvider, + region: string, + accountId: string | undefined +): Promise { + if (connection.name !== 'project.s3_default_folder' || !accountId) { + return [] + } + + return await listCallerAccessGrants(connectionCredentialsProvider, region, accountId, connection.connectionId) +} + +/** + * Creates a children provider function for a folder node + */ +function createFolderChildrenProvider(s3Client: S3Client, folderPath: any): (node: S3Node) => Promise { + const logger = getLogger('smus') + + return async (node: S3Node) => { + try { + // List objects in the folder + const allPaths = [] + let nextToken: string | undefined + + do { + const result = await s3Client.listPaths(folderPath.bucket, folderPath.prefix, nextToken) + allPaths.push(...result.paths) + nextToken = result.nextToken + } while (nextToken) + + if (allPaths.length === 0) { + return [createPlaceholderItem(NO_DATA_FOUND_MESSAGE) as S3Node] + } + + // Convert paths to nodes + return allPaths.map((path) => { + const nodeId = `${path.bucket}-${path.prefix || 'root'}` + + return new S3Node( + { + id: nodeId, + nodeType: path.isFolder ? NodeType.S3_FOLDER : NodeType.S3_FILE, + connectionType: ConnectionType.S3, + value: path, + path: { + connection: node.data.path?.connection, + bucket: path.bucket, + key: path.prefix, + label: path.displayName, + }, + parent: node, + }, + path.isFolder ? createFolderChildrenProvider(s3Client, path) : undefined + ) + }) + } catch (err) { + logger.error(`Failed to list folder contents: ${(err as Error).message}`) + const errorMessage = (err as Error).message + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'folder-contents', node.id) as S3Node] + } + } +} + +/** + * Parse S3 URI from connection + */ +function parseS3Uri(connection: DataZoneConnection): { bucket: string; prefix?: string } | undefined { + const s3Properties = connection.props?.s3Properties + const s3Uri = s3Properties?.s3Uri + + if (!s3Uri) { + return undefined + } + + // Handle case where s3Uri is just "s3://" (all buckets access) + if (s3Uri === 's3://') { + return { bucket: '', prefix: undefined } + } + + // Parse S3 URI: s3://bucket-name/prefix/path/ + const uriWithoutPrefix = s3Uri.replace('s3://', '') + + // Handle empty URI after removing prefix + if (!uriWithoutPrefix) { + return { bucket: '', prefix: undefined } + } + + // Since the URI ends with a slash, the last item will be an empty string, so ignore it in the parts. + const parts = uriWithoutPrefix.split('/').slice(0, -1) + const bucket = parts[0] || '' + + // If parts only contains 1 item, then only a bucket was provided, and the key is empty. + const prefix = parts.length > 1 ? parts.slice(1).join('/') + '/' : undefined + + return { bucket, prefix } +} + +async function listCallerAccessGrants( + connectionCredentialsProvider: ConnectionCredentialsProvider, + region: string, + accountId: string, + connectionId: string +): Promise { + const logger = getLogger('smus') + try { + const clientStore = ConnectionClientStore.getInstance() + const s3ControlClient = clientStore.getS3ControlClient(connectionId, region, connectionCredentialsProvider) + + const allGrants: ListCallerAccessGrantsEntry[] = [] + let nextToken: string | undefined + + do { + const command = new ListCallerAccessGrantsCommand({ + AccountId: accountId, + NextToken: nextToken, + }) + + const response = await s3ControlClient.send(command) + const grants = response.CallerAccessGrantsList?.filter((entry) => !!entry) ?? [] + allGrants.push(...grants) + nextToken = response.NextToken + } while (nextToken) + + logger.info(`Listed ${allGrants.length} caller access grants`) + + const accessGrantNodes = allGrants.map((grant) => + getRootNodeFromS3AccessGrant(grant, accountId, region, connectionCredentialsProvider, connectionId) + ) + return accessGrantNodes + } catch (error) { + logger.error(`Failed to list caller access grants: ${(error as Error).message}`) + await handleCredExpiredError(error) + return [] + } +} + +function parseS3UriForAccessGrant(s3Uri: string): { bucket: string; key: string } { + const uriWithoutPrefix = s3Uri.replace('s3://', '') + const parts = uriWithoutPrefix.split('/').slice(0, -1) + const bucket = parts[0] + const key = parts.length > 1 ? parts.slice(1).join('/') + '/' : '' + return { bucket, key } +} + +function getRootNodeFromS3AccessGrant( + s3AccessGrant: ListCallerAccessGrantsEntry, + accountId: string, + region: string, + connectionCredentialsProvider: ConnectionCredentialsProvider, + connectionId: string +): S3Node { + const s3Uri = s3AccessGrant.GrantScope + let bucket: string | undefined + let key: string | undefined + let nodeId = '' + let label: string + + if (s3Uri) { + const { bucket: parsedBucket, key: parsedKey } = parseS3UriForAccessGrant(s3Uri) + bucket = parsedBucket + key = parsedKey + label = s3Uri.replace('s3://', '').replace('*', '') + nodeId = label + } else { + label = s3AccessGrant.GrantScope ?? '' + } + + return new S3Node( + { + id: nodeId, + nodeType: NodeType.S3_ACCESS_GRANT, + connectionType: ConnectionType.S3, + value: s3AccessGrant, + path: { accountId, bucket, key, label }, + }, + async (node) => { + return await fetchAccessGrantChildren(node, accountId, region, connectionCredentialsProvider, connectionId) + } + ) +} + +async function fetchAccessGrantChildren( + node: S3Node, + accountId: string, + region: string, + connectionCredentialsProvider: ConnectionCredentialsProvider, + connectionId: string +): Promise { + const logger = getLogger('smus') + const path = node.data.path + + try { + const clientStore = ConnectionClientStore.getInstance() + const s3ControlClient = clientStore.getS3ControlClient(connectionId, region, connectionCredentialsProvider) + + const target = `s3://${path?.bucket ?? ''}/${path?.key ?? ''}*` + + const getDataAccessCommand = new GetDataAccessCommand({ + AccountId: accountId, + Target: target, + Permission: 'READ', + }) + + const grantCredentialsProvider = async () => { + const response = await s3ControlClient.send(getDataAccessCommand) + if ( + !response.Credentials?.AccessKeyId || + !response.Credentials?.SecretAccessKey || + !response.Credentials?.SessionToken + ) { + throw new Error('Missing required credentials from access grant response') + } + return { + accessKeyId: response.Credentials.AccessKeyId, + secretAccessKey: response.Credentials.SecretAccessKey, + sessionToken: response.Credentials.SessionToken, + expiration: response.Credentials.Expiration, + } + } + + const s3ClientWithGrant = new S3({ + credentials: grantCredentialsProvider, + region, + }) + + const response = await s3ClientWithGrant.send( + new ListObjectsV2Command({ + Bucket: path?.bucket ?? '', + Prefix: path?.key ?? '', + Delimiter: '/', + MaxKeys: 100, + }) + ) + + const children: S3Node[] = [] + + // Add folders + if (response.CommonPrefixes) { + for (const prefix of response.CommonPrefixes) { + const folderName = + prefix.Prefix?.split('/') + .filter((name) => !!name) + .at(-1) + '/' + children.push( + new S3Node( + { + id: `${node.id}${NODE_ID_DELIMITER}${folderName}`, + nodeType: NodeType.S3_FOLDER, + connectionType: ConnectionType.S3, + value: prefix, + path: { + accountId, + bucket: path?.bucket, + key: prefix.Prefix, + label: folderName, + }, + parent: node, + }, + async (folderNode) => { + return await fetchAccessGrantChildren( + folderNode, + accountId, + region, + connectionCredentialsProvider, + connectionId + ) + } + ) + ) + } + } + + // Add files + if (response.Contents) { + for (const content of response.Contents.filter((content) => content.Key !== response.Prefix)) { + const fileName = content.Key?.split('/').at(-1) ?? '' + children.push( + new S3Node({ + id: `${node.id}${NODE_ID_DELIMITER}${fileName}`, + nodeType: NodeType.S3_FILE, + connectionType: ConnectionType.S3, + value: content, + path: { + bucket: path?.bucket, + key: content.Key, + label: fileName, + }, + parent: node, + }) + ) + } + } + + return children + } catch (error) { + logger.error(`Failed to fetch access grant children: ${(error as Error).message}`) + const errorMessage = (error as Error).message + await handleCredExpiredError(error, true) + return [createErrorItem(errorMessage, 'access-grant-children', node.id) as S3Node] + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioAuthInfoNode.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioAuthInfoNode.ts new file mode 100644 index 00000000000..cca19378c22 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioAuthInfoNode.ts @@ -0,0 +1,121 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { SageMakerUnifiedStudioRootNode } from './sageMakerUnifiedStudioRootNode' +import { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider' +import { SmusIamConnection } from '../../auth/model' +import { getContext } from '../../../shared/vscode/setContext' +import { loadSharedConfigFiles } from '@smithy/shared-ini-file-loader' + +/** + * Node representing the SageMaker Unified Studio authentication information + */ +export class SageMakerUnifiedStudioAuthInfoNode implements TreeNode { + public readonly id = 'smusAuthInfoNode' + public readonly resource = this + private readonly authProvider: SmusAuthenticationProvider + + private readonly onDidChangeEmitter = new vscode.EventEmitter() + public readonly onDidChangeTreeItem = this.onDidChangeEmitter.event + + constructor(private readonly parent?: SageMakerUnifiedStudioRootNode) { + this.authProvider = SmusAuthenticationProvider.fromContext() + + // Subscribe to auth provider connection changes to refresh the node + this.authProvider.onDidChange(() => { + this.onDidChangeEmitter.fire() + }) + this.authProvider.onDidChangeActiveConnection(() => { + this.onDidChangeEmitter.fire() + }) + } + + public async getTreeItem(): Promise { + // Use the cached authentication provider to check connection status + const isConnected = this.authProvider.isConnected() + const isValid = this.authProvider.isConnectionValid() + + // Get the domain ID and region from auth provider + let domainId = 'Unknown' + let region = 'Unknown' + + if (isConnected && this.authProvider.activeConnection) { + domainId = this.authProvider.getDomainId() || 'Unknown' + region = this.authProvider.getDomainRegion() || 'Unknown' + } + + // Create display based on connection status + let label: string + let iconPath: vscode.ThemeIcon + let tooltip: string + let description: string | undefined + + // Get profile name for IAM mode + const isIamMode = getContext('aws.smus.isIamMode') + let profileName: string | undefined + if (isIamMode) { + const activeConnection = this.authProvider.activeConnection! + const { configFile } = await loadSharedConfigFiles() + profileName = + (activeConnection as SmusIamConnection).profileName || (configFile['default'] ? 'default' : undefined) + } + + if (isConnected && isValid) { + // Get session name and role ARN dynamically for IAM connections in IAM mode + let sessionName: string | undefined + let roleArn: string | undefined + if (isIamMode) { + sessionName = await this.authProvider.getSessionName() + roleArn = await this.authProvider.getIamPrincipalArn() + } + + // Format label with session name if available + const sessionSuffix = sessionName ? ` (session: ${sessionName})` : '' + label = isIamMode ? `Connected with profile: ${profileName}${sessionSuffix}` : `Domain: ${domainId}` + iconPath = new vscode.ThemeIcon('key', new vscode.ThemeColor('charts.green')) + + // Add role ARN and session name to tooltip if available (role ARN before session) + const roleArnTooltip = roleArn ? `\nRole ARN: ${roleArn}` : '' + const sessionTooltip = sessionName ? `\nSession: ${sessionName}` : '' + tooltip = `Connected to SageMaker Unified Studio\n${isIamMode ? `Profile: ${profileName}` : `Domain ID: ${domainId}`}\nRegion: ${region}${roleArnTooltip}${sessionTooltip}\nStatus: Connected` + description = region + } else if (isConnected && !isValid) { + label = isIamMode + ? `Profile: ${profileName} (Expired) - Click to reauthenticate` + : `Domain: ${domainId} (Expired) - Click to reauthenticate` + iconPath = new vscode.ThemeIcon('warning', new vscode.ThemeColor('charts.yellow')) + tooltip = `Connection to SageMaker Unified Studio has expired\n${isIamMode ? `Profile: ${profileName}` : `Domain ID: ${domainId}`}\nRegion: ${region}\nStatus: Expired - Click to reauthenticate` + description = region + } else { + label = 'Not Connected' + iconPath = new vscode.ThemeIcon('circle-slash', new vscode.ThemeColor('charts.red')) + tooltip = 'Not connected to SageMaker Unified Studio\nPlease sign in to access your projects' + description = undefined + } + + const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.None) + + // Add command for reauthentication when connection is expired + if (isConnected && !isValid) { + item.command = { + command: 'aws.smus.reauthenticate', + title: 'Reauthenticate', + arguments: [this.authProvider.activeConnection], + } + } + + item.tooltip = tooltip + item.contextValue = 'smusAuthInfo' + item.iconPath = iconPath + item.description = description + return item + } + + public getParent(): TreeNode | undefined { + return this.parent + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode.ts new file mode 100644 index 00000000000..eab4a58fbfb --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode.ts @@ -0,0 +1,69 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { getIcon } from '../../../shared/icons' +import { SageMakerUnifiedStudioSpacesParentNode } from './sageMakerUnifiedStudioSpacesParentNode' +import { SageMakerUnifiedStudioProjectNode } from './sageMakerUnifiedStudioProjectNode' +import { SagemakerClient } from '../../../shared/clients/sagemaker' +import { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider' +import { SageMakerUnifiedStudioConnectionParentNode } from './sageMakerUnifiedStudioConnectionParentNode' +import { ConnectionType } from '@aws-sdk/client-datazone' +import { getContext } from '../../../shared/vscode/setContext' + +export class SageMakerUnifiedStudioComputeNode implements TreeNode { + public readonly id = 'smusComputeNode' + public readonly resource = this + private spacesNode: SageMakerUnifiedStudioSpacesParentNode | undefined + + constructor( + public readonly parent: SageMakerUnifiedStudioProjectNode, + private readonly extensionContext: vscode.ExtensionContext, + public readonly authProvider: SmusAuthenticationProvider, + private readonly sagemakerClient: SagemakerClient + ) {} + + public async getTreeItem(): Promise { + const item = new vscode.TreeItem('Compute', vscode.TreeItemCollapsibleState.Expanded) + item.iconPath = getIcon('vscode-chip') + item.contextValue = this.getContext() + return item + } + + public async getChildren(): Promise { + const childrenNodes: TreeNode[] = [] + const projectId = this.parent.getProject()?.id + + if (projectId) { + if (!getContext('aws.smus.isIamMode')) { + childrenNodes.push( + new SageMakerUnifiedStudioConnectionParentNode(this, ConnectionType.REDSHIFT, 'Data warehouse') + ) + childrenNodes.push( + new SageMakerUnifiedStudioConnectionParentNode(this, ConnectionType.SPARK, 'Data processing') + ) + } + this.spacesNode = new SageMakerUnifiedStudioSpacesParentNode( + this, + projectId, + this.extensionContext, + this.authProvider, + this.sagemakerClient + ) + childrenNodes.push(this.spacesNode) + } + + return childrenNodes + } + + public getParent(): TreeNode | undefined { + return this.parent + } + + private getContext(): string { + return 'smusComputeNode' + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionNode.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionNode.ts new file mode 100644 index 00000000000..588d4f42062 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionNode.ts @@ -0,0 +1,63 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../../shared/logger/logger' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { SageMakerUnifiedStudioConnectionParentNode } from './sageMakerUnifiedStudioConnectionParentNode' +import { ConnectionSummary, ConnectionType } from '@aws-sdk/client-datazone' + +export class SageMakerUnifiedStudioConnectionNode implements TreeNode { + public resource: SageMakerUnifiedStudioConnectionNode + contextValue: string + private readonly logger = getLogger('smus') + id: string + public constructor( + private readonly parent: SageMakerUnifiedStudioConnectionParentNode, + private readonly connection: ConnectionSummary + ) { + this.id = connection.name ?? '' + this.resource = this + this.contextValue = this.getContext() + this.logger.debug(`SageMaker Space Node created: ${this.id}`) + } + + public async getTreeItem(): Promise { + const item = new vscode.TreeItem(this.id, vscode.TreeItemCollapsibleState.None) + item.contextValue = this.getContext() + item.tooltip = new vscode.MarkdownString(this.buildTooltip()) + return item + } + private buildTooltip(): string { + if (this.connection.type === ConnectionType.REDSHIFT) { + const tooltip = ''.concat( + '### Compute Details\n\n', + `**Type** \n${this.connection.type}\n\n`, + `**Environment ID** \n${this.connection.environmentId}\n\n`, + `**JDBC URL** \n${this.connection.props?.redshiftProperties?.jdbcUrl}` + ) + return tooltip + } else if (this.connection.type === ConnectionType.SPARK) { + const tooltip = ''.concat( + '### Compute Details\n\n', + `**Type** \n${this.connection.type}\n\n`, + `**Glue version** \n${this.connection.props?.sparkGlueProperties?.glueVersion}\n\n`, + `**Worker type** \n${this.connection.props?.sparkGlueProperties?.workerType}\n\n`, + `**Number of workers** \n${this.connection.props?.sparkGlueProperties?.numberOfWorkers}\n\n`, + `**Idle timeout (minutes)** \n${this.connection.props?.sparkGlueProperties?.idleTimeout}\n\n` + ) + return tooltip + } else { + return '' + } + } + private getContext(): string { + return 'SageMakerUnifiedStudioConnectionNode' + } + + public getParent(): TreeNode | undefined { + return this.parent + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionParentNode.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionParentNode.ts new file mode 100644 index 00000000000..3bb7fa80222 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionParentNode.ts @@ -0,0 +1,65 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { SageMakerUnifiedStudioComputeNode } from './sageMakerUnifiedStudioComputeNode' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { ListConnectionsCommandOutput, ConnectionType } from '@aws-sdk/client-datazone' +import { SageMakerUnifiedStudioConnectionNode } from './sageMakerUnifiedStudioConnectionNode' +import { createDZClientBaseOnDomainMode } from './utils' + +// eslint-disable-next-line id-length +export class SageMakerUnifiedStudioConnectionParentNode implements TreeNode { + public resource: SageMakerUnifiedStudioConnectionParentNode + contextValue: string + public connections: ListConnectionsCommandOutput | undefined + public constructor( + private readonly parent: SageMakerUnifiedStudioComputeNode, + private readonly connectionType: ConnectionType, + public id: string + ) { + this.resource = this + this.contextValue = this.getContext() + } + + public async getTreeItem(): Promise { + const item = new vscode.TreeItem(this.id, vscode.TreeItemCollapsibleState.Collapsed) + item.contextValue = this.getContext() + return item + } + + public async getChildren(): Promise { + const client = await createDZClientBaseOnDomainMode(this.parent.authProvider) + this.connections = await client.fetchConnections( + this.parent.parent.project?.domainId, + this.parent.parent.project?.id, + this.connectionType + ) + const childrenNodes = [] + if (!this.connections?.items || this.connections.items.length === 0) { + return [ + { + id: 'smusNoConnections', + resource: {}, + getTreeItem: () => + new vscode.TreeItem('[No connections found]', vscode.TreeItemCollapsibleState.None), + getParent: () => this, + }, + ] + } + for (const connection of this.connections.items) { + childrenNodes.push(new SageMakerUnifiedStudioConnectionNode(this, connection)) + } + return childrenNodes + } + + private getContext(): string { + return 'SageMakerUnifiedStudioConnectionParentNode' + } + + public getParent(): TreeNode | undefined { + return this.parent + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioDataNode.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioDataNode.ts new file mode 100644 index 00000000000..20d181bd1c3 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioDataNode.ts @@ -0,0 +1,315 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { getIcon } from '../../../shared/icons' + +import { getLogger } from '../../../shared/logger/logger' +import { DataZoneConnection, DataZoneProject } from '../../shared/client/datazoneClient' +import { createS3ConnectionNode, createS3AccessGrantNodes } from './s3Strategy' +import { createRedshiftConnectionNode } from './redshiftStrategy' +import { createLakehouseConnectionNode } from './lakehouseStrategy' +import { SageMakerUnifiedStudioProjectNode } from './sageMakerUnifiedStudioProjectNode' +import { isFederatedConnection, createErrorItem } from './utils' +import { createPlaceholderItem } from '../../../shared/treeview/utils' +import { + ConnectionType, + DATA_DEFAULT_S3_CONNECTION_NAME_REGEXP, + NO_DATA_FOUND_MESSAGE, + S3_PROJECT_NON_GIT_PROJECT_REPOSITORY_LOCATION_NAME_REGEXP, +} from './types' +import { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider' +import { createFederatedConnectionNode } from './federatedConnectionStrategy' +import { createDZClientForProject } from './utils' +import { getContext } from '../../../shared/vscode/setContext' +import { handleCredExpiredError } from '../../shared/credentialExpiryHandler' + +/** + * Tree node representing a Data folder that contains S3 and Redshift connections + */ +export class SageMakerUnifiedStudioDataNode implements TreeNode { + public readonly id = 'smusDataExplorer' + public readonly resource = {} + private readonly logger = getLogger('smus') + private childrenNodes: TreeNode[] | undefined + private readonly authProvider: SmusAuthenticationProvider + + constructor( + private readonly parent: SageMakerUnifiedStudioProjectNode, + initialChildren: TreeNode[] = [] + ) { + this.childrenNodes = initialChildren.length > 0 ? initialChildren : undefined + this.authProvider = SmusAuthenticationProvider.fromContext() + } + + public getTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem('Data', vscode.TreeItemCollapsibleState.Collapsed) + item.iconPath = getIcon('vscode-library') + item.contextValue = 'dataFolder' + return item + } + + public async getChildren(): Promise { + if (this.childrenNodes !== undefined) { + return this.childrenNodes + } + + try { + const project = this.parent.getProject() + if (!project) { + const errorMessage = 'No project information available' + this.logger.error(errorMessage) + void vscode.window.showErrorMessage(errorMessage) + return [createErrorItem(errorMessage, 'project', this.id)] + } + + const datazoneClient = await createDZClientForProject(this.authProvider, project.id) + + const connections = await datazoneClient.listConnections(project.domainId, undefined, project.id) + this.logger.info(`Found ${connections.length} connections for project ${project.id}`) + + if (connections.length === 0) { + this.childrenNodes = [createPlaceholderItem(NO_DATA_FOUND_MESSAGE)] + return this.childrenNodes + } + + const dataNodes = await this.createConnectionNodes(project, connections) + this.childrenNodes = dataNodes + return dataNodes + } catch (err) { + const project = this.parent.getProject() + const projectInfo = project ? `project: ${project.id}, domain: ${project.domainId}` : 'unknown project' + const errorMessage = 'Failed to get connections' + this.logger.error(`Failed to get connections for ${projectInfo}: ${(err as Error).message}`) + await handleCredExpiredError(err, true) + return [createErrorItem(errorMessage, 'connections', this.id)] + } + } + + public getParent(): TreeNode | undefined { + return this.parent + } + + private async createConnectionNodes( + project: DataZoneProject, + connections: DataZoneConnection[] + ): Promise { + const region = this.authProvider.getDomainRegion() + const dataNodes: TreeNode[] = [] + + const s3Connections = connections.filter((conn) => (conn.type as ConnectionType) === ConnectionType.S3) + const redshiftConnections = connections.filter( + (conn) => (conn.type as ConnectionType) === ConnectionType.REDSHIFT + ) + const lakehouseConnections = connections.filter( + (conn) => (conn.type as ConnectionType) === ConnectionType.LAKEHOUSE + ) + + // Add Lakehouse nodes first + for (const connection of lakehouseConnections) { + const node = await this.createLakehouseNode(project, connection, region) + dataNodes.push(node) + } + + // Add Redshift nodes second + if (!getContext('aws.smus.isIamMode')) { + for (const connection of redshiftConnections) { + if (connection.name.startsWith('project.lakehouse')) { + continue + } + if (isFederatedConnection(connection)) { + continue + } + const node = await this.createRedshiftNode(project, connection, region) + dataNodes.push(node) + } + } else { + const federatedConnections = connections.filter((conn) => isFederatedConnection(conn)) + if (federatedConnections.length > 0) { + const connectionsNode = this.createConnectionsParentNode(project, federatedConnections, region) + dataNodes.push(connectionsNode) + } + } + + // Add S3 Bucket parent node last + if (s3Connections.length > 0) { + const bucketNode = this.createBucketParentNode(project, s3Connections, region) + dataNodes.push(bucketNode) + } + + this.logger.info(`Created ${dataNodes.length} total connection nodes`) + return dataNodes + } + + private async createS3Node( + project: DataZoneProject, + connection: DataZoneConnection, + region: string + ): Promise { + try { + const connectionCredentialsProvider = await this.authProvider.getConnectionCredentialsProvider( + connection.connectionId, + project.id, + connection.location?.awsRegion || region + ) + + const s3ConnectionNode = createS3ConnectionNode( + connection, + connectionCredentialsProvider, + connection.location?.awsRegion || region + ) + + const accessGrantNodes = await createS3AccessGrantNodes( + connection, + connectionCredentialsProvider, + connection.location?.awsRegion || region, + connection.location?.awsAccountId + ) + + return [s3ConnectionNode, ...accessGrantNodes] + } catch (connErr) { + const errorMessage = `Failed to get S3 connection - ${(connErr as Error).message}` + this.logger.error(`Failed to get S3 connection details: ${(connErr as Error).message}`) + await handleCredExpiredError(connErr, true) + return [createErrorItem(errorMessage, `s3-${connection.connectionId}`, this.id)] + } + } + + private async createRedshiftNode( + project: DataZoneProject, + connection: DataZoneConnection, + region: string + ): Promise { + try { + const datazoneClient = await createDZClientForProject(this.authProvider, project.id) + const getConnectionResponse = await datazoneClient.getConnection({ + domainIdentifier: project.domainId, + identifier: connection.connectionId, + withSecret: true, + }) + + const connectionCredentialsProvider = await this.authProvider.getConnectionCredentialsProvider( + connection.connectionId, + project.id, + getConnectionResponse.location?.awsRegion || region + ) + + return createRedshiftConnectionNode(connection, connectionCredentialsProvider) + } catch (connErr) { + const errorMessage = `Failed to get Redshift connection - ${(connErr as Error).message}` + this.logger.error(`Failed to get Redshift connection details: ${(connErr as Error).message}`) + await handleCredExpiredError(connErr, true) + return createErrorItem(errorMessage, `redshift-${connection.connectionId}`, this.id) + } + } + + private async createLakehouseNode( + project: DataZoneProject, + connection: DataZoneConnection, + region: string + ): Promise { + try { + const connectionCredentialsProvider = await this.authProvider.getConnectionCredentialsProvider( + connection.connectionId, + project.id, + connection.location?.awsRegion || region + ) + + return createLakehouseConnectionNode(connection, connectionCredentialsProvider, region) + } catch (connErr) { + const errorMessage = `Failed to get Lakehouse connection - ${(connErr as Error).message}` + this.logger.error(`Failed to get Lakehouse connection details: ${(connErr as Error).message}`) + await handleCredExpiredError(connErr, true) + return createErrorItem(errorMessage, `lakehouse-${connection.connectionId}`, this.id) + } + } + + private createBucketParentNode( + project: DataZoneProject, + s3Connections: DataZoneConnection[], + region: string + ): TreeNode { + return { + id: 'bucket-parent', + resource: {}, + getTreeItem: () => { + const item = new vscode.TreeItem('Buckets', vscode.TreeItemCollapsibleState.Collapsed) + item.contextValue = 'bucketFolder' + return item + }, + getChildren: async () => { + // Filter connections inside the bucket parent node + const defaultS3Connection = s3Connections.find((conn) => + DATA_DEFAULT_S3_CONNECTION_NAME_REGEXP.test(conn.name) + ) + const otherS3Connections = s3Connections.filter( + (conn) => + !DATA_DEFAULT_S3_CONNECTION_NAME_REGEXP.test(conn.name) && + !S3_PROJECT_NON_GIT_PROJECT_REPOSITORY_LOCATION_NAME_REGEXP.test(conn.name) + ) + + const s3Nodes: TreeNode[] = [] + + // Add default connections first + if (defaultS3Connection) { + const defaultS3Node = await this.createS3Node(project, defaultS3Connection, region) + s3Nodes.push(...defaultS3Node) + } + + // Add other connections + for (const connection of otherS3Connections) { + const nodes = await this.createS3Node(project, connection, region) + s3Nodes.push(...nodes) + } + return s3Nodes + }, + getParent: () => this, + } + } + + private createConnectionsParentNode( + project: DataZoneProject, + federatedConnections: DataZoneConnection[], + region: string + ): TreeNode { + return { + id: 'connections-parent', + resource: {}, + getTreeItem: () => { + const item = new vscode.TreeItem('Connections', vscode.TreeItemCollapsibleState.Collapsed) + item.contextValue = 'connectionsFolder' + return item + }, + getChildren: async () => { + const nodes: TreeNode[] = [] + for (const connection of federatedConnections) { + try { + const connectionCredentialsProvider = await this.authProvider.getConnectionCredentialsProvider( + connection.connectionId, + project.id, + connection.location?.awsRegion || region + ) + const node = await createFederatedConnectionNode( + connection, + connectionCredentialsProvider, + region + ) + nodes.push(node) + } catch (err) { + const errorMessage = `Failed to create federated connection - ${(err as Error).message}` + this.logger.error( + `Failed to create federated connection ${connection.name}: ${(err as Error).message}` + ) + nodes.push(createErrorItem(errorMessage, `federated-${connection.connectionId}`, this.id)) + await handleCredExpiredError(err) + } + } + return nodes + }, + getParent: () => this, + } + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode.ts new file mode 100644 index 00000000000..6b41189b78b --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode.ts @@ -0,0 +1,286 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { getLogger } from '../../../shared/logger/logger' +import { telemetry } from '../../../shared/telemetry/telemetry' +import { AwsCredentialIdentity } from '@aws-sdk/types' +import { SageMakerUnifiedStudioDataNode } from './sageMakerUnifiedStudioDataNode' +import { DataZoneClient, DataZoneProject } from '../../shared/client/datazoneClient' +import { SageMakerUnifiedStudioRootNode } from './sageMakerUnifiedStudioRootNode' +import { SagemakerClient } from '../../../shared/clients/sagemaker' +import { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider' +import { SageMakerUnifiedStudioComputeNode } from './sageMakerUnifiedStudioComputeNode' +import { getIcon } from '../../../shared/icons' +import { getResourceMetadata } from '../../shared/utils/resourceMetadataUtils' +import { getContext } from '../../../shared/vscode/setContext' +import { ToolkitError } from '../../../shared/errors' +import { SmusErrorCodes } from '../../shared/smusUtils' +import { handleCredExpiredError } from '../../shared/credentialExpiryHandler' +import { SmusIamConnection } from '../../auth/model' +import { createDZClientBaseOnDomainMode, createErrorItem } from './utils' + +/** + * Tree node representing a SageMaker Unified Studio project + */ +export class SageMakerUnifiedStudioProjectNode implements TreeNode { + public readonly id = 'smusProjectNode' + public readonly resource = this + private readonly onDidChangeEmitter = new vscode.EventEmitter() + public readonly onDidChangeTreeItem = this.onDidChangeEmitter.event + public readonly onDidChangeChildren = this.onDidChangeEmitter.event + public project?: DataZoneProject + private logger = getLogger('smus') + private sagemakerClient?: SagemakerClient + private hasShownFirstTimeMessage = false + private isFirstTimeSelection = false + + constructor( + private readonly parent: SageMakerUnifiedStudioRootNode, + private readonly authProvider: SmusAuthenticationProvider, + private readonly extensionContext: vscode.ExtensionContext + ) { + // If we're in SMUS space environment, set project from resource metadata + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + const resourceMetadata = getResourceMetadata()! + if (resourceMetadata.AdditionalMetadata!.DataZoneProjectId) { + this.project = { + id: resourceMetadata!.AdditionalMetadata!.DataZoneProjectId!, + name: 'Current Project', + domainId: resourceMetadata!.AdditionalMetadata!.DataZoneDomainId!, + } + // Fetch the actual project name asynchronously + void this.fetchProjectName() + } + } + } + + public async getTreeItem(): Promise { + if (this.project) { + const item = new vscode.TreeItem('Project: ' + this.project.name, vscode.TreeItemCollapsibleState.Expanded) + item.contextValue = 'smusSelectedProject' + item.tooltip = `Project: ${this.project.name}\nID: ${this.project.id}` + item.iconPath = getIcon('vscode-folder-opened') + return item + } + + const item = new vscode.TreeItem('Select a project', vscode.TreeItemCollapsibleState.Expanded) + item.contextValue = 'smusProjectSelectPicker' + item.command = { + command: 'aws.smus.projectView', + title: 'Select Project', + arguments: [this], + } + item.iconPath = getIcon('vscode-folder-opened') + + return item + } + + public async getChildren(): Promise { + if (!this.project) { + return [] + } + + return telemetry.smus_renderProjectChildrenNode.run(async (span) => { + try { + const isInSmusSpace = getContext('aws.smus.inSmusSpaceEnvironment') + + // Get auth mode directly from connection type + const authMode = this.authProvider.activeConnection?.type + + const accountId = await this.authProvider.getDomainAccountId() + span.record({ + smusToolkitEnv: isInSmusSpace ? 'smus_space' : 'local', + smusDomainId: this.project?.domainId, + smusDomainAccountId: accountId, + smusProjectId: this.project?.id, + smusDomainRegion: this.authProvider.getDomainRegion(), + ...(authMode && { smusAuthMode: authMode }), + }) + + // Skip access check if we're in SMUS space environment (already in project space) + if (!getContext('aws.smus.inSmusSpaceEnvironment')) { + try { + const hasAccess = await this.checkProjectCredsAccess(this.project!.id) + if (!hasAccess) { + return [ + { + id: 'smusProjectAccessDenied', + resource: {}, + getTreeItem: () => { + const item = new vscode.TreeItem( + 'You do not have access to this project. Contact your administrator.', + vscode.TreeItemCollapsibleState.None + ) + return item + }, + getParent: () => this, + }, + ] + } + } catch (err) { + const errorMessage = (err as Error).message + this.logger.error('Failed to check project credentials: %s', errorMessage) + await handleCredExpiredError(err, true) + return [createErrorItem(`Failed to load the project`, this.project?.id || '', this.id)] + } + } + + const dataNode = new SageMakerUnifiedStudioDataNode(this) + + // If we're in SMUS space environment, only show data node + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + return [dataNode] + } + + const dzClient = await createDZClientBaseOnDomainMode(this.authProvider) + if (!this.project?.id) { + throw new Error('Project ID is required') + } + const toolingEnv = await dzClient.getToolingEnvironment(this.project.id) + const spaceAwsAccountRegion = toolingEnv.awsAccountRegion + + if (!spaceAwsAccountRegion) { + throw new Error('No AWS account region found in tooling environment') + } + if (this.isFirstTimeSelection && !this.hasShownFirstTimeMessage) { + this.hasShownFirstTimeMessage = true + void vscode.window.showInformationMessage( + 'Find your space in the Explorer panel under SageMaker Unified Studio. Hover over a space and click the connection icon to connect remotely.' + ) + } + this.sagemakerClient = await this.initializeSagemakerClient(spaceAwsAccountRegion) + const computeNode = new SageMakerUnifiedStudioComputeNode( + this, + this.extensionContext, + this.authProvider, + this.sagemakerClient + ) + return [dataNode, computeNode] + } catch (err) { + this.logger.error('Failed to select project: %s', (err as Error).message) + await handleCredExpiredError(err) + throw err + } + }) + } + + public getParent(): TreeNode | undefined { + return this.parent + } + + public async refreshNode(): Promise { + this.onDidChangeEmitter.fire() + } + + public async setProject(project: any): Promise { + await this.cleanupProjectResources() + this.isFirstTimeSelection = !this.project + this.project = project + } + + public getProject(): DataZoneProject | undefined { + return this.project + } + + public async clearProject(): Promise { + await this.cleanupProjectResources() + // Don't clear project if we're in SMUS space environment + if (!getContext('aws.smus.inSmusSpaceEnvironment')) { + this.project = undefined + } + await this.refreshNode() + } + + private async cleanupProjectResources(): Promise { + await this.authProvider.invalidateAllProjectCredentialsInCache() + if (this.sagemakerClient) { + this.sagemakerClient.dispose() + this.sagemakerClient = undefined + } + } + + private async checkProjectCredsAccess(projectId: string): Promise { + // TODO: Ideally we should be checking user project access by calling fetchAllProjectMemberships + // and checking if user is part of that, or get user groups and check if any of the groupIds + // exists in the project memberships for more comprehensive access validation. + try { + const projectProvider = await this.authProvider.getProjectCredentialProvider(projectId) + this.logger.info(`Successfully obtained project credentials provider for project ${projectId}`) + await projectProvider.getCredentials() + return true + } catch (err) { + // If err.name is 'AccessDeniedException', it means user doesn't have access to the project + // We can safely return false in that case without logging the error + if ((err as any).name === 'AccessDeniedException') { + this.logger.debug( + 'Access denied when obtaining project credentials, user likely lacks project access or role permissions' + ) + return false + } + throw err + } + } + + private async fetchProjectName(): Promise { + if (!this.project || !getContext('aws.smus.inSmusSpaceEnvironment')) { + return + } + + try { + const dzClient = await createDZClientBaseOnDomainMode(this.authProvider) + const projectDetails = await dzClient.getProject(this.project.id) + + if (projectDetails && projectDetails.name) { + this.project.name = projectDetails.name + // Refresh the tree item to show the updated name + this.onDidChangeEmitter.fire() + } + } catch (err) { + // No need to show error, this is just to dynamically show project name + // If we fail to fetch project name, we will just show the default name + this.logger.debug(`Failed to fetch project name: ${(err as Error).message}`) + } + } + + private async initializeSagemakerClient(regionCode: string): Promise { + if (!this.project) { + throw new Error('No project selected for initializing SageMaker client') + } + let awsCredentialProvider + if (getContext('aws.smus.isIamMode')) { + const datazoneClient = DataZoneClient.createWithCredentials( + this.authProvider.getDomainRegion(), + this.authProvider.getDomainId(), + await this.authProvider.getCredentialsProviderForIamProfile( + (this.authProvider.activeConnection as SmusIamConnection).profileName + ) + ) + const projectId = this.project.id + awsCredentialProvider = async (): Promise => { + const creds = await datazoneClient.getProjectDefaultEnvironmentCreds(projectId) + if (!creds.accessKeyId || !creds.secretAccessKey) { + throw new ToolkitError('Missing default environment credentials', { + code: SmusErrorCodes.CredentialRetrievalFailed, + }) + } + return { + accessKeyId: creds.accessKeyId!, + secretAccessKey: creds.secretAccessKey!, + sessionToken: creds.sessionToken, + } + } + } else { + const projectProvider = await this.authProvider.getProjectCredentialProvider(this.project.id) + this.logger.info(`Successfully obtained project credentials provider for project ${this.project.id}`) + awsCredentialProvider = async (): Promise => { + return await projectProvider.getCredentials() + } + } + const sagemakerClient = new SagemakerClient(regionCode, awsCredentialProvider) + return sagemakerClient + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioRootNode.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioRootNode.ts new file mode 100644 index 00000000000..2051ac7c52b --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioRootNode.ts @@ -0,0 +1,642 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { getIcon } from '../../../shared/icons' +import { getLogger } from '../../../shared/logger/logger' +import { DataZoneProject, DataZoneClient } from '../../shared/client/datazoneClient' +import { Commands } from '../../../shared/vscode/commands2' +import { telemetry } from '../../../shared/telemetry/telemetry' +import { createQuickPick } from '../../../shared/ui/pickerPrompter' +import { SageMakerUnifiedStudioProjectNode } from './sageMakerUnifiedStudioProjectNode' +import { SageMakerUnifiedStudioAuthInfoNode } from './sageMakerUnifiedStudioAuthInfoNode' +import { SmusErrorCodes, SmusUtils } from '../../shared/smusUtils' +import { handleCredExpiredError } from '../../shared/credentialExpiryHandler' +import { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider' +import { ToolkitError } from '../../../../src/shared/errors' +import { SmusAuthenticationMethod } from '../../auth/ui/authenticationMethodSelection' +import { SmusAuthenticationOrchestrator } from '../../auth/authenticationOrchestrator' +import { isSmusSsoConnection, isSmusIamConnection } from '../../auth/model' +import { getContext } from '../../../shared/vscode/setContext' +import { createDZClientBaseOnDomainMode } from './utils' +import { DataZoneCustomClientHelper } from '../../shared/client/datazoneCustomClientHelper' +import { recordAuthTelemetry } from '../../shared/telemetry' + +const contextValueSmusRoot = 'sageMakerUnifiedStudioRoot' +const contextValueSmusLogin = 'sageMakerUnifiedStudioLogin' +const contextValueSmusLearnMore = 'sageMakerUnifiedStudioLearnMore' +const projectPickerTitle = 'Select a SageMaker Unified Studio project you want to open' +const projectPickerPlaceholder = 'Select project' + +export class SageMakerUnifiedStudioRootNode implements TreeNode { + public readonly id = 'smusRootNode' + public readonly resource = this + private readonly logger = getLogger('smus') + private readonly projectNode: SageMakerUnifiedStudioProjectNode + private readonly authInfoNode: SageMakerUnifiedStudioAuthInfoNode + private readonly onDidChangeEmitter = new vscode.EventEmitter() + public readonly onDidChangeTreeItem = this.onDidChangeEmitter.event + public readonly onDidChangeChildren = this.onDidChangeEmitter.event + + public constructor( + private readonly authProvider: SmusAuthenticationProvider, + private readonly extensionContext: vscode.ExtensionContext + ) { + this.authInfoNode = new SageMakerUnifiedStudioAuthInfoNode(this) + this.projectNode = new SageMakerUnifiedStudioProjectNode(this, this.authProvider, this.extensionContext) + + // Subscribe to auth provider connection changes to refresh the node + this.authProvider.onDidChange(async () => { + // Clear the project when connection changes + await this.projectNode.clearProject() + this.onDidChangeEmitter.fire() + // Immediately refresh the tree view to show authenticated state + try { + await vscode.commands.executeCommand('aws.smus.rootView.refresh') + } catch (refreshErr) { + this.logger.debug( + `Failed to refresh views after connection state change: ${(refreshErr as Error).message}` + ) + } + }) + } + + public getTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem('SageMaker Unified Studio', vscode.TreeItemCollapsibleState.Expanded) + item.contextValue = contextValueSmusRoot + item.iconPath = getIcon('vscode-database') + + // Set description based on authentication state + if (!this.isAuthenticated()) { + item.description = 'Not authenticated' + } else { + item.description = 'Connected' + } + + return item + } + + public async getChildren(): Promise { + const isAuthenticated = this.isAuthenticated() + const hasExpiredConnection = this.hasExpiredConnection() + + this.logger.debug( + `SMUS Root Node getChildren: isAuthenticated=${isAuthenticated}, hasExpiredConnection=${hasExpiredConnection}` + ) + + // Check for expired connection first + if (hasExpiredConnection) { + // Show auth info node with expired indication + return [this.authInfoNode] // This will show expired connection info + } + + // Check authentication state + if (!isAuthenticated) { + // Show login option and learn more link when not authenticated + return [ + { + id: 'smusLogin', + resource: {}, + getTreeItem: () => { + const item = new vscode.TreeItem('Sign in to get started', vscode.TreeItemCollapsibleState.None) + item.contextValue = contextValueSmusLogin + item.iconPath = getIcon('vscode-account') + + // Set up the login command + item.command = { + command: 'aws.smus.login', + title: 'Sign in to SageMaker Unified Studio', + } + + return item + }, + getParent: () => this, + }, + { + id: 'smusLearnMore', + resource: {}, + getTreeItem: () => { + const item = new vscode.TreeItem( + 'Learn more about SageMaker Unified Studio', + vscode.TreeItemCollapsibleState.None + ) + item.contextValue = contextValueSmusLearnMore + item.iconPath = getIcon('vscode-question') + + // Set up the learn more command + item.command = { + command: 'aws.smus.learnMore', + title: 'Learn more about SageMaker Unified Studio', + } + + return item + }, + getParent: () => this, + }, + ] + } + + // When authenticated, show auth info and projects (same for both IAM and non-IAM mode) + return [this.authInfoNode, this.projectNode] + } + + public getProjectSelectNode(): SageMakerUnifiedStudioProjectNode { + return this.projectNode + } + + public getAuthInfoNode(): SageMakerUnifiedStudioAuthInfoNode { + return this.authInfoNode + } + + public refresh(): void { + this.onDidChangeEmitter.fire() + } + + /** + * Checks if the user has authenticated to SageMaker Unified Studio + * This is validated by checking existing Connections for SMUS or resource metadata. + */ + private isAuthenticated(): boolean { + try { + // Check if the connection is valid using the authentication provider + const result = this.authProvider.isConnectionValid() + this.logger.debug(`Authentication check result: ${result}`) + return result + } catch (err) { + this.logger.debug('Authentication check failed: %s', (err as Error).message) + return false + } + } + + private hasExpiredConnection(): boolean { + try { + const activeConnection = this.authProvider.activeConnection + const isConnectionValid = this.authProvider.isConnectionValid() + + this.logger.debug( + `SMUS Root Node: activeConnection=${!!activeConnection}, isConnectionValid=${isConnectionValid}` + ) + + // Check if there's an active connection but it's expired/invalid + const hasExpiredConnection = activeConnection && !isConnectionValid + + if (hasExpiredConnection) { + this.logger.debug('Connection is expired') + // Only show reauthentication prompt for SSO connections, not IAM connections + if (isSmusSsoConnection(activeConnection)) { + this.logger.debug('Showing reauthentication prompt for SSO connection') + void this.authProvider.showReauthenticationPrompt(activeConnection) + } else { + this.logger.debug('Skipping reauthentication prompt for non-SSO connection') + } + return true + } + return false + } catch (err) { + this.logger.debug('Failed to check expired connection: %s', (err as Error).message) + return false + } + } +} + +/** + * Command to open the SageMaker Unified Studio documentation + */ +export const smusLearnMoreCommand = Commands.declare('aws.smus.learnMore', () => async () => { + const logger = getLogger('smus') + try { + // Open the SageMaker Unified Studio documentation + await vscode.env.openExternal(vscode.Uri.parse('https://aws.amazon.com/sagemaker/unified-studio/')) + + // Log telemetry + telemetry.record({ + name: 'smus_learnMoreClicked', + result: 'Succeeded', + passive: false, + }) + } catch (err) { + logger.error('Failed to open SageMaker Unified Studio documentation: %s', (err as Error).message) + + // Log failure telemetry + telemetry.record({ + name: 'smus_learnMoreClicked', + result: 'Failed', + passive: false, + }) + } +}) + +/** + * Command to login to SageMaker Unified Studio + */ +export const smusLoginCommand = Commands.declare('aws.smus.login', (context: vscode.ExtensionContext) => async () => { + const logger = getLogger('smus') + return telemetry.smus_login.run(async (span) => { + try { + // Get the authentication provider instance + const authProvider = SmusAuthenticationProvider.fromContext() + + // Import authentication method selection components + const { SmusAuthenticationMethodSelector } = await import('../../auth/ui/authenticationMethodSelection.js') + const { SmusAuthenticationPreferencesManager } = await import( + '../../auth/preferences/authenticationPreferences.js' + ) + + // Check for preferred authentication method + const preferredMethod = SmusAuthenticationPreferencesManager.getPreferredMethod(context) + logger.debug(`Retrieved preferred method: ${preferredMethod}`) + + let selectedMethod: SmusAuthenticationMethod | undefined = preferredMethod + let authCompleted = false + + // Main authentication loop - handles back navigation + while (!authCompleted) { + // Check if we should skip method selection (user has a remembered preference) + if (selectedMethod) { + logger.debug(`Using authentication method: ${selectedMethod}`) + } else { + // Show authentication method selection dialog + logger.debug('Showing authentication method selection dialog') + const methodSelection = await SmusAuthenticationMethodSelector.showAuthenticationMethodSelection() + selectedMethod = methodSelection.method + } + + // Handle the selected authentication method + logger.debug(`Processing authentication method: ${selectedMethod}`) + if (selectedMethod === 'sso') { + // SSO Authentication - use SSO flow + const ssoResult = await SmusAuthenticationOrchestrator.handleSsoAuthentication( + authProvider, + span, + context + ) + + if (ssoResult.status === 'BACK') { + // User wants to go back to authentication method selection + selectedMethod = undefined // Reset to show method selection again + continue // Restart the loop + } + + authCompleted = true + } else { + // IAM Authentication - use new IAM profile selection flow + const iamResult = await SmusAuthenticationOrchestrator.handleIamAuthentication( + authProvider, + span, + context + ) + + if (iamResult.status === 'BACK') { + // User wants to go back to authentication method selection + selectedMethod = undefined // Reset to show method selection again + continue // Restart the loop + } + + if (iamResult.status === 'EDITING') { + // User is editing credentials, show helpful message with option to return to profile selection + const action = await vscode.window.showInformationMessage( + 'Complete your AWS credential setup and try again, or return to profile selection.', + 'Select Profile', + 'Done' + ) + + if (action === 'Select Profile') { + // User wants to return to profile selection, continue the loop + continue + } else { + // User chose "Done" or dismissed, exit the authentication flow + throw new ToolkitError('User cancelled credential setup', { + code: SmusErrorCodes.UserCancelled, + cancelled: true, + }) + } + } + + if (iamResult.status === 'INVALID_PROFILE') { + // Profile validation failed, show error with option to select another profile + const action = await vscode.window.showErrorMessage( + `${iamResult.error}`, + 'Select Another Profile', + 'Cancel' + ) + + if (action === 'Select Another Profile') { + // User wants to select a different profile, continue the loop + continue + } else { + // User chose "Cancel" or dismissed, exit the authentication flow + throw new ToolkitError('User cancelled profile selection', { + code: SmusErrorCodes.UserCancelled, + cancelled: true, + }) + } + } + + authCompleted = true + } + } + + // Record telemetry with connection details after successful login + const domainId = authProvider.getDomainId?.() + const region = authProvider.getDomainRegion?.() + await recordAuthTelemetry(span, authProvider, domainId, region) + } catch (err) { + const isUserCancelled = err instanceof ToolkitError && err.code === SmusErrorCodes.UserCancelled + if (!isUserCancelled) { + void vscode.window.showErrorMessage(`Failed to initiate login: ${(err as Error).message}`) + logger.error('Failed to initiate login: %s', (err as Error).message) + } + throw err + } + }) +}) + +/** + * Command to sign out from SageMaker Unified Studio + */ +export const smusSignOutCommand = Commands.declare( + 'aws.smus.signOut', + (context: vscode.ExtensionContext) => async () => { + const logger = getLogger('smus') + return telemetry.smus_signOut.run(async (span) => { + try { + // Get the authentication provider instance + const authProvider = SmusAuthenticationProvider.fromContext() + + // Check if there's an active connection to sign out from + if (!authProvider.isConnected()) { + void vscode.window.showInformationMessage( + 'No active SageMaker Unified Studio connection to sign out from.' + ) + return + } + + // Capture connection details BEFORE signing out (for telemetry) + const activeConnection = authProvider.activeConnection + const domainId = authProvider.getDomainId?.() + const region = authProvider.getDomainRegion?.() + + // Record telemetry with captured values BEFORE signing out + await recordAuthTelemetry(span, authProvider, domainId, region) + + // Sign out from SMUS (behavior depends on connection type) + if (activeConnection) { + await authProvider.signOut() + logger.info(`Signed out from SageMaker Unified Studio: ${domainId}`) + + // Clear connection-specific preferences on sign out (but keep auth method preference) + const { SmusAuthenticationPreferencesManager } = await import( + '../../auth/preferences/authenticationPreferences.js' + ) + await SmusAuthenticationPreferencesManager.clearConnectionPreferences(context) + } + + // Show success message + void vscode.window.showInformationMessage('Successfully signed out from SageMaker Unified Studio.') + + // Refresh the tree view to show the sign-in state + try { + await vscode.commands.executeCommand('aws.smus.rootView.refresh') + } catch (refreshErr) { + logger.debug(`Failed to refresh views after sign out: ${(refreshErr as Error).message}`) + throw new ToolkitError('Failed to refresh views after sign out.', { + cause: refreshErr as Error, + code: (refreshErr as Error).name, + }) + } + } catch (err) { + void vscode.window.showErrorMessage( + `SageMaker Unified Studio: Failed to sign out: ${(err as Error).message}` + ) + logger.error('Failed to sign out: %s', (err as Error).message) + + // Log failure telemetry + throw new ToolkitError('Failed to sign out.', { + cause: err as Error, + code: (err as Error).name, + }) + } + }) + } +) + +function isAccessDenied(error: Error): boolean { + return error.name.includes('AccessDenied') +} + +function createProjectQuickPickItems(projects: DataZoneProject[]) { + return projects + .sort( + (a, b) => + (b.updatedAt ? new Date(b.updatedAt).getTime() : 0) - + (a.updatedAt ? new Date(a.updatedAt).getTime() : 0) + ) + .filter((project) => project.name !== 'GenerativeAIModelGovernanceProject') + .map((project) => ({ + label: project.name, + detail: 'ID: ' + project.id, + description: project.description, + data: project, + })) +} + +async function showQuickPick(items: any[]) { + const quickPick = createQuickPick(items, { + title: projectPickerTitle, + placeholder: projectPickerPlaceholder, + }) + return await quickPick.prompt() +} + +/** + * Fetches projects filtered by IAM principal + * For IAM users: filters by user profile using userIdentifier + * For IAM role sessions: filters by group profile using groupIdentifier + * @param authProvider The SMUS authentication provider + * @param datazoneClient The DataZone client instance + * @returns Promise resolving to filtered projects array + * @throws Error if profile retrieval fails + */ +async function fetchProjectsByIamProfile( + authProvider: SmusAuthenticationProvider, + datazoneClient: DataZoneClient +): Promise { + const logger = getLogger('smus') + + // Get credentials provider for IAM profile + const activeConnection = authProvider.activeConnection + if (!isSmusIamConnection(activeConnection)) { + throw new Error('Active connection is not a valid IAM connection') + } + + // Use cached caller identity ARN from auth provider + const callerIdentityArn = await authProvider.getIamPrincipalArn() + if (!callerIdentityArn) { + throw new Error('Unable to retrieve caller identity ARN from cache') + } + + // Determine if this is an IAM user or IAM role session using utility method + const isIamUser = SmusUtils.isIamUserArn(callerIdentityArn) + logger.debug( + `Using cached caller identity ARN: ${callerIdentityArn}. Identity type: ${isIamUser ? 'IAM User' : 'IAM Role Session'}` + ) + + let projects: DataZoneProject[] + + if (isIamUser) { + // IAM User flow - use GetUserProfile and filter by userIdentifier + logger.debug('Using IAM user flow with GetUserProfile API') + + // Get user profile ID for the IAM user using DataZone client + const userProfileId = await datazoneClient.getUserProfileIdForIamPrincipal( + callerIdentityArn, + authProvider.getDomainId() + ) + logger.info(`Retrieved user profile ID: ${userProfileId} for IAM principal ${callerIdentityArn}`) + + // Fetch projects filtered by user profile + projects = await datazoneClient.fetchAllProjects({ userIdentifier: userProfileId }) + logger.debug(`Fetched ${projects.length} projects for user profile ${userProfileId}`) + } else { + const credentialsProvider = await authProvider.getCredentialsProviderForIamProfile(activeConnection.profileName) + const datazoneCustomClientHelper = DataZoneCustomClientHelper.getInstance( + credentialsProvider, + authProvider.getDomainRegion() + ) + + // IAM Role Session flow - use SearchGroupProfile and filter by groupIdentifier + // The cached ARN needs conversion for role sessions + const roleArn = SmusUtils.convertAssumedRoleArnToIamRoleArn(callerIdentityArn) + logger.debug(`Using IAM role ARN: ${roleArn}`) + + // Get group profile ID for the current role + const groupProfileId = await datazoneCustomClientHelper.getGroupProfileId(authProvider.getDomainId(), roleArn) + logger.info(`Retrieved group profile ID: ${groupProfileId}`) + + // Fetch projects filtered by group profile + projects = await datazoneClient.fetchAllProjects({ groupIdentifier: groupProfileId }) + logger.debug(`Fetched ${projects.length} projects for group profile ${groupProfileId}`) + } + + return projects +} + +export async function selectSMUSProject(projectNode?: SageMakerUnifiedStudioProjectNode) { + const logger = getLogger('smus') + + return telemetry.smus_accessProject.run(async (span) => { + try { + const authProvider = SmusAuthenticationProvider.fromContext() + if (!authProvider.activeConnection) { + logger.error('No active connection to display project view') + return + } + + const datazoneClient = await createDZClientBaseOnDomainMode(authProvider) + logger.debug('DataZone client instance obtained successfully') + + let allProjects: DataZoneProject[] + + if (getContext('aws.smus.isIamMode')) { + // Filter projects by IAM profile (user or role session) + try { + allProjects = await fetchProjectsByIamProfile(authProvider, datazoneClient) + } catch (err) { + const error = err as Error + + // Handle no profile found (user or group) + if ( + error instanceof ToolkitError && + (error.code === SmusErrorCodes.NoGroupProfileFound || + error.code === SmusErrorCodes.NoUserProfileFound) + ) { + logger.error('No profile found for IAM principal: %s', error.message) + + const principalArn = await authProvider.getIamPrincipalArn() + const arnSuffix = principalArn ? `: ${principalArn}` : '' + void vscode.window.showErrorMessage( + `No resources found for IAM principal${arnSuffix}. Ensure SageMaker Unified Studio resources exist for this IAM principal.` + ) + return error + } + + // Handle access denied + if (isAccessDenied(error)) { + logger.error('Access denied when retrieving profile: %s', error.message) + void vscode.window.showErrorMessage( + "You don't have permissions to access this resource. Please contact your administrator" + ) + return error + } + + // Handle other errors + logger.error('Failed to retrieve profile information: %s', error.message) + void vscode.window.showErrorMessage('Failed to fetch IAM principal information. Try again.') + return error + } + } else { + // In non-IAM mode, fetch all projects without filtering + allProjects = await datazoneClient.fetchAllProjects() + } + + const items = createProjectQuickPickItems(allProjects) + + // Handle no projects scenario + if (items.length === 0) { + if (getContext('aws.smus.isIamMode')) { + logger.debug('No accessible projects found for IAM principal') + void vscode.window.showInformationMessage('No accessible projects found for your IAM principal') + } else { + logger.debug('No projects found in the domain') + void vscode.window.showInformationMessage('No projects found in the domain') + } + return + } + + // Show project picker + const selectedProject = await showQuickPick(items) + + const accountId = await authProvider.getDomainAccountId() + span.record({ + smusAuthMode: authProvider.activeConnection?.type, + smusDomainId: authProvider.getDomainId(), + smusProjectId: (selectedProject as DataZoneProject).id as string | undefined, + smusDomainRegion: authProvider.getDomainRegion(), + smusDomainAccountId: accountId, + }) + if ( + selectedProject && + typeof selectedProject === 'object' && + selectedProject !== null && + !('type' in selectedProject) && + projectNode + ) { + await projectNode.setProject(selectedProject) + await vscode.commands.executeCommand('aws.smus.rootView.refresh') + } + + return selectedProject + } catch (err) { + const error = err as Error + + // Handle access denied scenarios + if (isAccessDenied(error)) { + logger.error('Access denied when fetching projects: %s', error.message) + await showQuickPick([ + { + label: '$(error)', + description: "You don't have permissions to view projects. Please contact your administrator", + }, + ]) + return + } + + // Handle network/API failures + logger.error('Failed to select project: %s', error.message) + await handleCredExpiredError(err, true) + } + }) +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode.ts new file mode 100644 index 00000000000..53ae501d967 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode.ts @@ -0,0 +1,108 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { SagemakerClient, SagemakerSpaceApp } from '../../../shared/clients/sagemaker' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { SageMakerUnifiedStudioSpacesParentNode } from './sageMakerUnifiedStudioSpacesParentNode' +import { SagemakerSpace } from '../../../awsService/sagemaker/sagemakerSpace' + +export class SagemakerUnifiedStudioSpaceNode implements TreeNode { + private smSpace: SagemakerSpace + private readonly onDidChangeEmitter = new vscode.EventEmitter() + public readonly onDidChangeTreeItem = this.onDidChangeEmitter.event + public readonly onDidChangeChildren = this.onDidChangeEmitter.event + + public constructor( + private readonly parent: SageMakerUnifiedStudioSpacesParentNode, + public readonly sageMakerClient: SagemakerClient, + public readonly regionCode: string, + public readonly spaceApp: SagemakerSpaceApp, + isSMUSSpace: boolean + ) { + this.smSpace = new SagemakerSpace(this.sageMakerClient, this.regionCode, this.spaceApp, isSMUSSpace) + } + + public getTreeItem(): vscode.TreeItem { + return { + label: this.smSpace.label, + description: this.smSpace.description, + tooltip: this.smSpace.tooltip, + iconPath: this.smSpace.iconPath, + contextValue: this.smSpace.contextValue, + collapsibleState: vscode.TreeItemCollapsibleState.None, + } + } + + public getChildren(): TreeNode[] { + return [] + } + + public getParent(): TreeNode | undefined { + return this.parent + } + + public async refreshNode(): Promise { + this.onDidChangeEmitter.fire() + } + + public get id(): string { + return 'smusSpaceNode' + this.name + } + + public get resource() { + return this + } + + // Delegate all core functionality to SageMakerSpace instance + public updateSpace(spaceApp: SagemakerSpaceApp) { + this.smSpace.updateSpace(spaceApp) + if (this.isPending()) { + this.parent.trackPendingNode(this.DomainSpaceKey) + } + } + + public setSpaceStatus(spaceStatus: string, appStatus: string) { + this.smSpace.setSpaceStatus(spaceStatus, appStatus) + } + public isPending(): boolean { + return this.smSpace.isPending() + } + public getStatus(): string { + return this.smSpace.getStatus() + } + public async getAppStatus() { + return this.smSpace.getAppStatus() + } + public get name(): string { + return this.smSpace.name + } + public get arn(): string { + return this.smSpace.arn + } + public async getAppArn() { + return this.smSpace.getAppArn() + } + public async getSpaceArn() { + return this.smSpace.getSpaceArn() + } + public async updateSpaceAppStatus() { + await this.smSpace.updateSpaceAppStatus() + + if (this.isPending()) { + this.parent.trackPendingNode(this.DomainSpaceKey) + } + return + } + public buildTooltip() { + return this.smSpace.buildTooltip() + } + public getAppIcon() { + return this.smSpace.getAppIcon() + } + public get DomainSpaceKey(): string { + return this.smSpace.DomainSpaceKey + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode.ts new file mode 100644 index 00000000000..d2d7e4a8173 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode.ts @@ -0,0 +1,372 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { SageMakerUnifiedStudioComputeNode } from './sageMakerUnifiedStudioComputeNode' +import { updateInPlace } from '../../../shared/utilities/collectionUtils' +import { DescribeDomainResponse } from '@amzn/sagemaker-client' +import { getDomainUserProfileKey } from '../../../awsService/sagemaker/utils' +import { getLogger } from '../../../shared/logger/logger' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { SagemakerClient, SagemakerSpaceApp } from '../../../shared/clients/sagemaker' +import { UserProfileMetadata } from '../../../awsService/sagemaker/explorer/sagemakerStudioNode' +import { SagemakerUnifiedStudioSpaceNode } from './sageMakerUnifiedStudioSpaceNode' +import { PollingSet } from '../../../shared/utilities/pollingSet' +import { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider' +import { SmusUtils, SmusErrorCodes } from '../../shared/smusUtils' +import { getIcon } from '../../../shared/icons' +import { PENDING_NODE_POLLING_INTERVAL_MS } from './utils' +import { getContext } from '../../../shared/vscode/setContext' +import { createDZClientBaseOnDomainMode } from './utils' +import { SmusIamConnection } from '../../auth/model' +import { DataZoneCustomClientHelper } from '../../shared/client/datazoneCustomClientHelper' +import { ToolkitError } from '../../../shared/errors' + +export class SageMakerUnifiedStudioSpacesParentNode implements TreeNode { + public readonly id = 'smusSpacesParentNode' + public readonly resource = this + private readonly sagemakerSpaceNodes: Map = new Map() + private spaceApps: Map = new Map() + private domainUserProfiles: Map = new Map() + private readonly logger = getLogger('smus') + private readonly onDidChangeEmitter = new vscode.EventEmitter() + public readonly onDidChangeTreeItem = this.onDidChangeEmitter.event + public readonly onDidChangeChildren = this.onDidChangeEmitter.event + public readonly pollingSet: PollingSet = new PollingSet( + PENDING_NODE_POLLING_INTERVAL_MS, + this.updatePendingNodes.bind(this) + ) + private spaceAwsAccountRegion: string | undefined + + public constructor( + private readonly parent: SageMakerUnifiedStudioComputeNode, + private readonly projectId: string, + private readonly extensionContext: vscode.ExtensionContext, + private readonly authProvider: SmusAuthenticationProvider, + private readonly sagemakerClient: SagemakerClient + ) {} + + public async getTreeItem(): Promise { + const item = new vscode.TreeItem('Spaces', vscode.TreeItemCollapsibleState.Expanded) + item.iconPath = { + light: vscode.Uri.joinPath( + this.extensionContext.extensionUri, + 'resources/icons/aws/sagemakerunifiedstudio/spaces-dark.svg' + ), + dark: vscode.Uri.joinPath( + this.extensionContext.extensionUri, + 'resources/icons/aws/sagemakerunifiedstudio/spaces.svg' + ), + } + item.contextValue = 'smusSpacesNode' + item.description = 'Hover over any space and click the connection icon to connect remotely' + item.tooltip = item.description + return item + } + + public async getChildren(): Promise { + try { + await this.updateChildren() + } catch (err) { + const error = err as Error + if (error.name === 'AccessDeniedException') { + return this.getAccessDeniedChildren() + } + // Handle no profile found (user or group) using error codes + if ( + error instanceof ToolkitError && + (error.code === SmusErrorCodes.NoGroupProfileFound || error.code === SmusErrorCodes.NoUserProfileFound) + ) { + return await this.getNoUserProfileChildren() + } + if (error.message.includes('Failed to retrieve user profile information')) { + return this.getUserProfileErrorChildren(error.message) + } + return this.getNoSpacesFoundChildren() + } + const nodes = [...this.sagemakerSpaceNodes.values()] + if (nodes.length === 0) { + return this.getNoSpacesFoundChildren() + } + return nodes + } + + private getNoSpacesFoundChildren(): TreeNode[] { + return [ + { + id: 'smusNoSpaces', + resource: {}, + getTreeItem: () => new vscode.TreeItem('[No Spaces found]', vscode.TreeItemCollapsibleState.None), + getParent: () => this, + }, + ] + } + + private getAccessDeniedChildren(): TreeNode[] { + return [ + { + id: 'smusAccessDenied', + resource: {}, + getTreeItem: () => { + const item = new vscode.TreeItem( + "You don't have permission to view spaces. Please contact your administrator.", + vscode.TreeItemCollapsibleState.None + ) + item.iconPath = getIcon('vscode-error') + return item + }, + getParent: () => this, + }, + ] + } + + private async getNoUserProfileChildren(): Promise { + // Log the IAM principal ARN for debugging + const principalArn = await this.authProvider.getIamPrincipalArn() + if (principalArn) { + this.logger.error(`No spaces found for IAM principal: ${principalArn}`) + } + + return [ + { + id: 'smusNoUserProfile', + resource: {}, + getTreeItem: () => { + const item = new vscode.TreeItem( + 'No spaces found for IAM principal', + vscode.TreeItemCollapsibleState.None + ) + item.iconPath = getIcon('vscode-error') + return item + }, + getParent: () => this, + }, + ] + } + + private getUserProfileErrorChildren(message: string): TreeNode[] { + return [ + { + id: 'smusUserProfileError', + resource: {}, + getTreeItem: () => { + const item = new vscode.TreeItem( + 'Failed to retrieve spaces. Please try again.', + vscode.TreeItemCollapsibleState.None + ) + item.iconPath = getIcon('vscode-error') + return item + }, + getParent: () => this, + }, + ] + } + + public getParent(): TreeNode | undefined { + return this.parent + } + + public getProjectId(): string { + return this.projectId + } + + public getAuthProvider(): SmusAuthenticationProvider { + return this.authProvider + } + + public async refreshNode(): Promise { + this.onDidChangeEmitter.fire() + } + + public trackPendingNode(domainSpaceKey: string) { + this.pollingSet.add(domainSpaceKey) + } + + public getSpaceNodes(spaceKey: string): SagemakerUnifiedStudioSpaceNode { + const childNode = this.sagemakerSpaceNodes.get(spaceKey) + if (childNode) { + return childNode + } else { + throw new Error(`Node with id ${spaceKey} from polling set not found`) + } + } + + public async getSageMakerDomainId(): Promise { + const activeConnection = this.authProvider.activeConnection + if (!activeConnection) { + this.logger.error('There is no active connection to get SageMaker domain ID') + throw new Error('No active connection found to get SageMaker domain ID') + } + + this.logger.debug('Getting DataZone client instance') + const datazoneClient = await createDZClientBaseOnDomainMode(this.authProvider) + if (!datazoneClient) { + throw new Error('DataZone client is not initialized') + } + + const toolingEnv = await datazoneClient.getToolingEnvironment(this.projectId) + this.spaceAwsAccountRegion = toolingEnv.awsAccountRegion + if (toolingEnv.provisionedResources) { + for (const resource of toolingEnv.provisionedResources) { + if (resource.name === 'sageMakerDomainId') { + if (!resource.value) { + throw new Error('SageMaker domain ID not found in tooling environment') + } + getLogger('smus').debug(`Found SageMaker domain ID: ${resource.value}`) + return resource.value + } + } + } + throw new Error('No SageMaker domain found in the tooling environment') + } + + private async updatePendingNodes() { + for (const spaceKey of this.pollingSet.values()) { + const childNode = this.getSpaceNodes(spaceKey) + await this.updatePendingSpaceNode(childNode) + } + } + + private async updatePendingSpaceNode(node: SagemakerUnifiedStudioSpaceNode) { + await node.updateSpaceAppStatus() + if (!node.isPending()) { + this.pollingSet.delete(node.DomainSpaceKey) + await node.refreshNode() + } + } + + /** + * Retrieves the user profile ID for IAM mode (IAM authentication) + * @returns Promise resolving to the user profile ID + * @throws Error if user profile retrieval fails + */ + private async getUserProfileIdForIamAuthMode(): Promise { + try { + // Get cached caller IAM identity ARN from auth provider + const callerArn = await this.authProvider.getIamPrincipalArn() + + if (!callerArn) { + throw new Error('Unable to retrieve caller identity ARN') + } + // Determine if this is an IAM user or role session based on ARN format + if (SmusUtils.isIamUserArn(callerArn)) { + // For IAM users, use GetUserProfile API directly via DataZoneClient + this.logger.debug(`Detected IAM user, using GetUserProfile API with ARN: ${callerArn}`) + + const datazoneClient = await createDZClientBaseOnDomainMode(this.authProvider) + const userProfileId = await datazoneClient.getUserProfileIdForIamPrincipal( + callerArn, + this.authProvider.getDomainId() + ) + + if (!userProfileId) { + throw new ToolkitError('No user profile found for IAM user') + } + + this.logger.debug(`Retrieved user profile ID for IAM user: ${userProfileId}`) + return userProfileId + } else { + // For IAM role sessions, use SearchUserProfile API via DataZoneCustomClientHelper + // Need to get the full assumed role ARN (with session) for filtering + const assumedRoleArn = await this.authProvider.getCachedIamCallerIdentityArn() + + if (!assumedRoleArn) { + throw new Error('Unable to retrieve assumed role ARN with session') + } + + this.logger.debug( + `SMUS: Detected IAM role session, using SearchUserProfile API with ARN: ${assumedRoleArn}` + ) + + // Get credentials provider for the IAM profile + const credentialsProvider = await this.authProvider.getCredentialsProviderForIamProfile( + (this.authProvider.activeConnection as SmusIamConnection).profileName + ) + + const datazoneCustomClientHelper = DataZoneCustomClientHelper.getInstance( + credentialsProvider, + this.authProvider.getDomainRegion() + ) + + const userProfileId = await datazoneCustomClientHelper.getUserProfileIdForSession( + this.authProvider.getDomainId(), + assumedRoleArn + ) + + this.logger.debug(`Retrieved user profile ID for role session: ${userProfileId}`) + return userProfileId + } + } catch (err) { + const error = err as Error + this.logger.error(`Failed to retrieve user profile information: ${error.message}`) + + if (error.name === 'AccessDeniedException') { + throw new Error("You don't have permissions to access this resource. Please contact your administrator") + } + throw err + } + } + + private async updateChildren(): Promise { + const datazoneClient = await createDZClientBaseOnDomainMode(this.authProvider) + + let userProfileId + if (getContext('aws.smus.isIamMode')) { + userProfileId = await this.getUserProfileIdForIamAuthMode() + } else { + // Will be of format: 'ABCA4NU3S7PEOLDQPLXYZ:user-12345678-d061-70a4-0bf2-eeee67a6ab12' + const userId = await datazoneClient.getUserId() + userProfileId = SmusUtils.extractSSOIdFromUserId(userId || '') + } + + const sagemakerDomainId = await this.getSageMakerDomainId() + const [spaceApps, domains] = await this.sagemakerClient.fetchSpaceAppsAndDomains( + sagemakerDomainId, + false /* filterSmusDomains */ + ) + + // Filter spaceApps to only show spaces owned by current user + this.logger.debug(`Filtering spaces for user profile ID: ${userProfileId}`) + const filteredSpaceApps = new Map() + for (const [key, app] of spaceApps.entries()) { + const userProfile = app.OwnershipSettingsSummary?.OwnerUserProfileName + if (userProfileId === userProfile) { + filteredSpaceApps.set(key, app) + } + } + this.spaceApps = filteredSpaceApps + this.domainUserProfiles.clear() + + for (const app of this.spaceApps.values()) { + const domainId = app.DomainId + const userProfile = app.OwnershipSettingsSummary?.OwnerUserProfileName + if (!domainId || !userProfile) { + continue + } + + const domainUserProfileKey = getDomainUserProfileKey(domainId, userProfile) + this.domainUserProfiles.set(domainUserProfileKey, { + domain: domains.get(domainId) as DescribeDomainResponse, + }) + } + + updateInPlace( + this.sagemakerSpaceNodes, + this.spaceApps.keys(), + (key) => this.sagemakerSpaceNodes.get(key)!.updateSpace(this.spaceApps.get(key)!), + (key) => + new SagemakerUnifiedStudioSpaceNode( + this as any, + this.sagemakerClient, + this.spaceAwsAccountRegion || + (() => { + throw new Error('No AWS account region found in tooling environment') + })(), + this.spaceApps.get(key)!, + true /* isSMUSSpace */ + ) + ) + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/types.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/types.ts new file mode 100644 index 00000000000..73da925a8f7 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/types.ts @@ -0,0 +1,229 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Node delimiter for creating unique IDs +// eslint-disable-next-line @typescript-eslint/naming-convention +export const NODE_ID_DELIMITER = '/' + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const AWS_DATA_CATALOG = 'AwsDataCatalog' +// eslint-disable-next-line @typescript-eslint/naming-convention +export const DATA_DEFAULT_IAM_CONNECTION_NAME_REGEXP = /^(project\.iam)|(default\.iam)$/ +// eslint-disable-next-line @typescript-eslint/naming-convention, id-length +export const DATA_DEFAULT_LAKEHOUSE_CONNECTION_NAME_REGEXP = /^(project\.default_lakehouse)|(default\.catalog)$/ +// eslint-disable-next-line @typescript-eslint/naming-convention, id-length +export const DATA_DEFAULT_ATHENA_CONNECTION_NAME_REGEXP = /^(project\.athena)|(default\.sql)$/ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const DATA_DEFAULT_S3_CONNECTION_NAME_REGEXP = /^(project\.s3_default_folder)|(default\.s3)$/ +// eslint-disable-next-line @typescript-eslint/naming-convention, id-length +export const S3_PROJECT_NON_GIT_PROJECT_REPOSITORY_LOCATION_NAME_REGEXP = + /^(project\.non_git_project_repository_location)|(default\.s3_shared)$/ + +// Database object types +export enum DatabaseObjects { + EXTERNAL_TABLE = 'EXTERNAL_TABLE', + VIRTUAL_VIEW = 'VIRTUAL_VIEW', +} + +// Ref: https://docs.aws.amazon.com/athena/latest/ug/data-types.html +export const lakeHouseColumnTypes = { + NUMERIC: ['TINYINT', 'SMALLINT', 'INT', 'INTEGER', 'BIGINT', 'FLOAT', 'REAL', 'DOUBLE', 'DECIMAL'], + STRING: ['CHAR', 'STRING', 'VARCHAR', 'UUID'], + TIME: ['DATE', 'TIMESTAMP', 'INTERVAL'], + BOOLEAN: ['BOOLEAN'], + BINARY: ['BINARY', 'VARBINARY'], + COMPLEX: ['ARRAY', 'MAP', 'STRUCT', 'ROW', 'JSON'], +} + +// Ref: https://docs.aws.amazon.com/redshift/latest/dg/c_Supported_data_types.html +export const redshiftColumnTypes = { + NUMERIC: ['SMALLINT', 'INT2', 'INTEGER', 'INT', 'BIGINT', 'DECIMAL', 'NUMERIC', 'REAL', 'FLOAT', 'DOUBLE'], + STRING: ['CHAR', 'CHARACTER', 'NCHAR', 'BPCHAR', 'VARCHAR', 'VARCHAR', 'VARYING', 'NVARCHAR', 'TEXT'], + TIME: ['TIME', 'TIMETZ', 'TIMESTAMP', 'TIMESTAMPTZ', 'INTERVAL'], + BOOLEAN: ['BOOLEAN', 'BOOL'], + BINARY: ['VARBYTE', 'VARBINARY', 'BINARY', 'VARYING'], + COMPLEX: ['HLLSKETCH', 'SUPER', 'GEOMETRY', 'GEOGRAPHY'], +} + +/** + * Node types for different resources + */ +export enum NodeType { + // Common types + CONNECTION = 'connection', + ERROR = 'error', + LOADING = 'loading', + EMPTY = 'empty', + + // S3 types + S3_BUCKET = 's3-bucket', + S3_FOLDER = 'folder', + S3_FILE = 'file', + S3_ACCESS_GRANT = 's3-access-grant', + + // Redshift types + REDSHIFT_CLUSTER = 'redshift-cluster', + REDSHIFT_DATABASE = 'database', + REDSHIFT_SCHEMA = 'schema', + REDSHIFT_TABLE = 'table', + REDSHIFT_VIEW = 'view', + REDSHIFT_FUNCTION = 'function', + REDSHIFT_STORED_PROCEDURE = 'storedProcedure', + REDSHIFT_COLUMN = 'column', + REDSHIFT_CONTAINER = 'container', + + // Glue types + GLUE_CATALOG = 'catalog', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + GLUE_DATABASE = 'database', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + GLUE_TABLE = 'table', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + GLUE_VIEW = 'view', + + // Redshift-specific catalog types + REDSHIFT_CATALOG = 'redshift-catalog', + REDSHIFT_CATALOG_DATABASE = 'redshift-catalog-database', +} + +/** + * Connection types + */ +export enum ConnectionType { + S3 = 'S3', + REDSHIFT = 'REDSHIFT', + ATHENA = 'ATHENA', + GLUE = 'GLUE', + LAKEHOUSE = 'LAKEHOUSE', +} + +/** + * Resource types for Redshift + */ +export enum ResourceType { + DATABASE = 'DATABASE', + CATALOG_DATABASE = 'CATALOG_DATABASE', + SCHEMA = 'SCHEMA', + TABLE = 'TABLE', + VIEW = 'VIEW', + FUNCTION = 'FUNCTION', + STORED_PROCEDURE = 'STORED_PROCEDURE', + COLUMNS = 'COLUMNS', + CATALOG = 'CATALOG', + EXTERNAL_DATABASE = 'EXTERNAL_DATABASE', + SHARED_DATABASE = 'SHARED_DATABASE', + EXTERNAL_SCHEMA = 'EXTERNAL_SCHEMA', + SHARED_SCHEMA = 'SHARED_SCHEMA', + EXTERNAL_TABLE = 'EXTERNAL_TABLE', + CATALOG_TABLE = 'CATALOG_TABLE', + DATA_CATALOG_TABLE = 'DATA_CATALOG_TABLE', + CATALOG_COLUMN = 'CATALOG_COLUMN', +} + +/** + * Node path information + */ +export interface NodePath { + connection?: string + bucket?: string + key?: string + catalog?: string + database?: string + schema?: string + table?: string + column?: string + cluster?: string + label?: string + [key: string]: any +} + +/** + * Node data interface for tree nodes + */ +export interface NodeData { + id: string + nodeType: NodeType + connectionType?: ConnectionType + value?: any + path?: NodePath + parent?: any + isContainer?: boolean + children?: any[] +} + +/** + * Redshift deployment types + */ +export enum RedshiftType { + Serverless = 'SERVERLESS', + ServerlessDev = 'SERVERLESS_DEV', + ServerlessQA = 'SERVERLESS_QA', + Cluster = 'CLUSTER', + ClusterDev = 'CLUSTER_DEV', + ClusterQA = 'CLUSTER_QA', +} + +/** + * Authentication types for database integration connections + */ +export enum DatabaseIntegrationConnectionAuthenticationTypes { + FEDERATED = '4', + TEMPORARY_CREDENTIALS_WITH_IAM = '5', + SECRET = '6', + IDC_ENHANCED_IAM_CREDENTIALS = '8', +} + +/** + * Redshift service model URLs + */ +export const RedshiftServiceModelUrl = { + REDSHIFT_SERVERLESS_URL: 'redshift-serverless.amazonaws.com', + REDSHIFT_CLUSTER_URL: 'redshift.amazonaws.com', +} + +/** + * Client types for ClientStore + */ +export enum ClientType { + S3Client = 'S3Client', + S3ControlClient = 'S3ControlClient', + SQLWorkbenchClient = 'SQLWorkbenchClient', + GlueClient = 'GlueClient', + GlueCatalogClient = 'GlueCatalogClient', +} + +/** + * Node types that are always leaf nodes + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const LEAF_NODE_TYPES = [ + NodeType.S3_FILE, + NodeType.REDSHIFT_COLUMN, + NodeType.ERROR, + NodeType.LOADING, + NodeType.EMPTY, +] + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const NO_DATA_FOUND_MESSAGE = '[No data found]' + +/** + * Glue connection types + */ +export const glueConnectionTypes = [ + 'BIGQUERY', + 'DOCUMENTDB', + 'DYNAMODB', + 'MYSQL', + 'OPENSEARCH', + 'ORACLE', + 'POSTGRESQL', + 'REDSHIFT', + 'SAPHANA', + 'SNOWFLAKE', + 'SQLSERVER', + 'TERADATA', + 'VERTICA', +] diff --git a/packages/core/src/sagemakerunifiedstudio/explorer/nodes/utils.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/utils.ts new file mode 100644 index 00000000000..64c2ffa64b7 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/utils.ts @@ -0,0 +1,538 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getIcon, IconPath, addColor } from '../../../shared/icons' +import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' +import { + NODE_ID_DELIMITER, + NodeType, + RedshiftServiceModelUrl, + RedshiftType, + ConnectionType, + NodeData, + LEAF_NODE_TYPES, + DATA_DEFAULT_LAKEHOUSE_CONNECTION_NAME_REGEXP, + redshiftColumnTypes, + lakeHouseColumnTypes, + glueConnectionTypes, +} from './types' +import { DataZoneClient, DataZoneConnection } from '../../shared/client/datazoneClient' +import { getContext } from '../../../shared/vscode/setContext' +import { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider' +import { SmusIamConnection } from '../../auth/model' +import { ConnectionStatus } from '@aws-sdk/client-datazone' +import { Catalog } from '@amzn/glue-catalog-client' + +/** + * Polling interval in milliseconds for checking space status updates + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const PENDING_NODE_POLLING_INTERVAL_MS = 5000 + +/** + * Check if a catalog is a RedLake catalog + */ +export const isRedLakeCatalog = (catalog?: Catalog) => { + return ( + catalog?.FederatedCatalog?.ConnectionName === 'aws:redshift' || + catalog?.CatalogProperties?.DataLakeAccessProperties?.CatalogType === 'aws:redshift' + ) +} + +/** + * Check if a catalog is a S3 table catalog + */ +export const isS3TablesCatalog = (catalog?: Catalog) => { + return catalog?.FederatedCatalog?.ConnectionName === 'aws:s3tables' +} + +/** + * Gets the label for a node based on its data + */ +export function getLabel(data: { + id: string + nodeType: NodeType + isContainer?: boolean + path?: { key?: string; label?: string } + value?: any +}): string { + // For S3 access grant nodes, use S3 (label) format + if (data.nodeType === NodeType.S3_ACCESS_GRANT && data.path?.label) { + return `S3 (${data.path.label})` + } + + // For connection nodes, use the connection name + if (data.nodeType === NodeType.CONNECTION && data.value?.connection?.name) { + if ( + data.value?.connection?.type === ConnectionType.LAKEHOUSE && + DATA_DEFAULT_LAKEHOUSE_CONNECTION_NAME_REGEXP.test(data.value?.connection?.name) + ) { + if (getContext('aws.smus.isIamMode')) { + return 'Catalogs' + } + return 'Lakehouse' + } + const formattedType = data.value?.connection?.type?.replace(/([A-Z]+(?:_[A-Z]+)*)/g, (match: string) => { + const words = match.split('_') + return words.map((word: string) => word.charAt(0) + word.slice(1).toLowerCase()).join(' ') + }) + return `${formattedType} (${data.value.connection.name})` + } + + // For container nodes, use the node type + if (data.isContainer) { + switch (data.nodeType) { + case NodeType.REDSHIFT_TABLE: + return 'Tables' + case NodeType.REDSHIFT_VIEW: + return 'Views' + case NodeType.REDSHIFT_FUNCTION: + return 'Functions' + case NodeType.REDSHIFT_STORED_PROCEDURE: + return 'Stored Procedures' + default: + return data.nodeType + } + } + + // For path-based nodes, use the last part of the path + if (data.path?.label) { + return data.path.label + } + + // For S3 folders, add a trailing slash + if (data.nodeType === NodeType.S3_FOLDER) { + const key = data.path?.key || '' + const parts = key.split('/') + return parts[parts.length - 2] + '/' + } + + // For S3 files, use the filename + if (data.nodeType === NodeType.S3_FILE) { + const key = data.path?.key || '' + const parts = key.split('/') + return parts[parts.length - 1] + } + + // For other nodes, use the last part of the ID + const parts = data.id.split(NODE_ID_DELIMITER) + return parts[parts.length - 1] +} + +/** + * Determines if a node is a leaf node + */ +export function isLeafNode(data: { nodeType: NodeType; isContainer?: boolean }): boolean { + // Container nodes are never leaf nodes + if (data.isContainer) { + return false + } + + return LEAF_NODE_TYPES.includes(data.nodeType) +} + +/** + * Gets the icon for a node type + */ +export function getIconForNodeType(nodeType: NodeType, isContainer?: boolean): vscode.ThemeIcon | IconPath | undefined { + switch (nodeType) { + case NodeType.CONNECTION: + case NodeType.S3_ACCESS_GRANT: + return undefined + case NodeType.S3_BUCKET: + return getIcon('aws-s3-bucket') + case NodeType.S3_FOLDER: + return getIcon('vscode-folder') + case NodeType.S3_FILE: + return getIcon('vscode-file') + case NodeType.REDSHIFT_CLUSTER: + return getIcon('aws-redshift-cluster') + case NodeType.REDSHIFT_DATABASE: + case NodeType.GLUE_DATABASE: + return new vscode.ThemeIcon('database') + case NodeType.REDSHIFT_SCHEMA: + return getIcon('aws-redshift-schema') + case NodeType.REDSHIFT_TABLE: + case NodeType.GLUE_TABLE: + return isContainer ? new vscode.ThemeIcon('table') : getIcon('aws-redshift-table') + case NodeType.REDSHIFT_VIEW: + return isContainer ? new vscode.ThemeIcon('list-tree') : new vscode.ThemeIcon('eye') + case NodeType.REDSHIFT_FUNCTION: + case NodeType.REDSHIFT_STORED_PROCEDURE: + return isContainer ? new vscode.ThemeIcon('list-tree') : new vscode.ThemeIcon('symbol-method') + case NodeType.GLUE_CATALOG: + return getIcon('aws-sagemakerunifiedstudio-catalog') + case NodeType.REDSHIFT_CATALOG: + return new vscode.ThemeIcon('database') + case NodeType.REDSHIFT_CATALOG_DATABASE: + return getIcon('aws-redshift-schema') + case NodeType.ERROR: + return new vscode.ThemeIcon('error') + case NodeType.LOADING: + return new vscode.ThemeIcon('loading~spin') + case NodeType.EMPTY: + return new vscode.ThemeIcon('info') + default: + return getIcon('vscode-circle-outline') + } +} + +/** + * Creates a standard tree item for a node + */ +export function createTreeItem( + label: string, + nodeType: NodeType, + isLeaf: boolean, + isContainer?: boolean, + tooltip?: string +): vscode.TreeItem { + const collapsibleState = isLeaf ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed + + const item = new vscode.TreeItem(label, collapsibleState) + + // Set icon based on node type + item.iconPath = getIconForNodeType(nodeType, isContainer) + + // Set context value for command enablement + item.contextValue = nodeType + + // Set tooltip if provided + if (tooltip) { + item.tooltip = tooltip + } + + return item +} + +/** + * Gets the column type category from a raw column type string + */ +export function getColumnType(columnTypeString?: string): string { + if (!columnTypeString) { + return 'UNKNOWN' + } + + const lowerType = columnTypeString.toLowerCase() + + // Search in both redshift and lakehouse column types + const allTypes = [...Object.values(redshiftColumnTypes).flat(), ...Object.values(lakeHouseColumnTypes).flat()].map( + (type) => type.toLowerCase() + ) + + return allTypes.find((key) => lowerType.startsWith(key)) || 'UNKNOWN' +} + +/** + * Gets the icon for a column based on its type + */ +function getColumnIcon(columnType: string): vscode.ThemeIcon | IconPath { + const upperType = columnType.toUpperCase() + + // Check if it's a numeric type + if ( + lakeHouseColumnTypes.NUMERIC.some((type) => upperType.includes(type)) || + redshiftColumnTypes.NUMERIC.some((type) => upperType.includes(type)) + ) { + return getIcon('aws-sagemakerunifiedstudio-symbol-int') + } + + // Check if it's a string type + if ( + lakeHouseColumnTypes.STRING.some((type) => upperType.includes(type)) || + redshiftColumnTypes.STRING.some((type) => upperType.includes(type)) + ) { + return getIcon('vscode-symbol-key') + } + + // Check if it's a time type + if ( + lakeHouseColumnTypes.TIME.some((type) => upperType.includes(type)) || + redshiftColumnTypes.TIME.some((type) => upperType.includes(type)) + ) { + return getIcon('vscode-calendar') + } + + // Check if it's a boolean type + if ( + lakeHouseColumnTypes.BOOLEAN.some((type) => upperType.includes(type)) || + redshiftColumnTypes.BOOLEAN.some((type) => upperType.includes(type)) + ) { + return getIcon('vscode-symbol-boolean') + } + + // Default icon for unknown types + return new vscode.ThemeIcon('symbol-field') +} + +/** + * Creates a tree item for a column node with type information + */ +export function createColumnTreeItem(label: string, columnType: string, nodeType: NodeType): vscode.TreeItem { + const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.None) + + // Add column type as description (secondary text) + item.description = columnType + + // Set icon based on column type + item.iconPath = getColumnIcon(columnType) + + // Set context value for command enablement + item.contextValue = nodeType + + // Set tooltip + item.tooltip = `${label}: ${columnType}` + + return item +} + +/** + * Creates an error node + */ +export function createErrorTreeItem(message: string): vscode.TreeItem { + const item = new vscode.TreeItem(message, vscode.TreeItemCollapsibleState.None) + item.iconPath = new vscode.ThemeIcon('error') + return item +} + +/** + * Creates an error item with unique ID and proper styling + */ +export function createErrorItem(message: string, context: string, parentId: string): TreeNode { + return { + id: `${parentId}-error-${context}-${Date.now()}`, + resource: message, + getTreeItem: () => { + const item = new vscode.TreeItem(message, vscode.TreeItemCollapsibleState.None) + item.iconPath = addColor(getIcon('vscode-error'), 'testing.iconErrored') + return item + }, + } +} + +export const isRedLakeDatabase = (databaseName?: string) => { + if (!databaseName) { + return false + } + const regex = /[\w\d\-_]+@[\w\d\-_]+/gs + return regex.test(databaseName) +} + +/** + * Gets the tooltip for a node + * @param data The node data + * @returns The tooltip text + */ +export function getTooltip(data: NodeData): string { + const label = getLabel(data) + + switch (data.nodeType) { + // Common node types + case NodeType.CONNECTION: + return data.connectionType === ConnectionType.REDSHIFT + ? `Redshift Connection: ${label}` + : `Connection: ${label}\nType: ${data.connectionType}` + + // S3 node types + case NodeType.S3_BUCKET: + return `S3 Bucket: ${data.path?.bucket}` + case NodeType.S3_FOLDER: + return `Folder: ${label}\nBucket: ${data.path?.bucket}` + case NodeType.S3_FILE: + return `File: ${label}\nBucket: ${data.path?.bucket}` + + // Redshift node types + case NodeType.REDSHIFT_CLUSTER: + return `Redshift Cluster: ${label}` + case NodeType.REDSHIFT_DATABASE: + return `Database: ${label}` + case NodeType.REDSHIFT_SCHEMA: + return `Schema: ${label}` + case NodeType.REDSHIFT_TABLE: + return data.isContainer ? `Tables in ${data.path?.schema}` : `Table: ${data.path?.schema}.${label}` + case NodeType.REDSHIFT_VIEW: + return data.isContainer ? `Views in ${data.path?.schema}` : `View: ${data.path?.schema}.${label}` + case NodeType.REDSHIFT_FUNCTION: + return data.isContainer ? `Functions in ${data.path?.schema}` : `Function: ${data.path?.schema}.${label}` + case NodeType.REDSHIFT_STORED_PROCEDURE: + return data.isContainer + ? `Stored Procedures in ${data.path?.schema}` + : `Stored Procedure: ${data.path?.schema}.${label}` + + // Glue node types + case NodeType.GLUE_CATALOG: + return `Glue Catalog: ${label}` + case NodeType.GLUE_DATABASE: + return `Glue Database: ${label}` + case NodeType.GLUE_TABLE: + return `Glue Table: ${label}` + + // Default + default: + return label + } +} + +/** + * Gets the Redshift type from a host + * @param host Redshift host + * @returns Redshift type or null if not recognized + */ +export function getRedshiftTypeFromHost(host?: string): RedshiftType | undefined { + /* + 'default-workgroup.{accountID}.us-west-2.redshift-serverless.amazonaws.com' - SERVERLESS + 'default-rs-cluster.{id}.us-west-2.redshift.amazonaws.com' - CLUSTER + 'default-rs-cluster.{id}.us-west-2.redshift.amazonaws.com:5439/dev' - CLUSTER + */ + if (!host) { + return undefined + } + + const cleanHost = host.split(':')[0] + const parts = cleanHost.split('.') + if (parts.length < 3) { + return undefined + } + + const domain = parts.slice(parts.length - 3).join('.') + + if (domain === RedshiftServiceModelUrl.REDSHIFT_SERVERLESS_URL) { + return RedshiftType.Serverless + } else if (domain === RedshiftServiceModelUrl.REDSHIFT_CLUSTER_URL) { + return RedshiftType.Cluster + } else { + return undefined + } +} + +/** + * This function searches for property keys that end with "Properties" (like "snowflakeProperties", + * "redshiftProperties", "athenaProperties") and returns the actual property object, not just the key name. + * It only works for connections that have a glueConnectionName, indicating they are federated connections. + * + * @param connection - The DataZone connection object to search + * @returns The property object (not the key name) if found, undefined otherwise + * + * @example + * ```typescript + * // Redshift connection + * const redshiftConnection = { + * glueConnectionName: 'my-redshift-glue-conn', + * props: { + * redshiftProperties: { + * status: 'FAILED', + * errorMessage: 'Connection timeout' + * } + * } + * } + * const result = getGluePropertiesKey(redshiftConnection) + * // Returns: { status: 'FAILED', errorMessage: 'Connection timeout' } + */ +export function getGluePropertiesKey(connection: DataZoneConnection) { + if (!connection?.props) { + return undefined + } + if (!connection.glueConnectionName) { + return undefined + } + // Check for other properties that might contain glue connection info + const propertiesKey = Object.keys(connection.props).find( + (key) => + key.endsWith('Properties') && + typeof connection.props![key] === 'object' && + !Array.isArray(connection.props![key]) + ) + + return propertiesKey ? connection.props[propertiesKey] : undefined +} + +/** + * This function handles the refactor where connections moved from a single `glueProperties` object to + * connector-specific property bags (like `snowflakeProperties`, `redshiftProperties`, `athenaProperties`). + * It first checks for the legacy `glueProperties` field, then falls back to connector-specific properties. + * + * @param connection - The DataZone connection object to extract properties from + * @returns Object with optional status and errorMessage fields, or undefined if no properties found + */ +export function getGlueProperties(connection?: DataZoneConnection) { + if (!connection?.props) { + return undefined + } + // Check for direct glueProperties + if ('glueProperties' in connection.props) { + return connection.props.glueProperties + } + + return connection?.props?.[getGluePropertiesKey(connection)!] as + | { status?: ConnectionStatus; errorMessage?: string } + | undefined +} + +/** + * Determines if a connection is a federated connection by checking its type. + * A connection is considered federated if it's either: + * 1. A Redshift connection with Glue properties, or + * 2. A connection type that exists in GlueConnectionType + * + * @param connection + * @returns - boolean + */ +export function isFederatedConnection(connection?: DataZoneConnection): boolean { + if (connection?.type === ConnectionType.REDSHIFT) { + return !!getGlueProperties(connection) + } + + // Check if connection type exists in GlueConnectionType enum values + return glueConnectionTypes.includes(connection?.type || '') +} + +/** + * Creates a DataZoneClient with appropriate credentials provider based on domain mode + * If domain mode is IAM mode, use the credential profile credential provider + * If domain mode is not IAM mode, use the DER credential provider + * @param smusAuthProvider The SMUS authentication provider + * @returns Promise resolving to DataZoneClient instance + */ +export async function createDZClientBaseOnDomainMode( + smusAuthProvider: SmusAuthenticationProvider +): Promise { + let credentialsProvider + if (getContext('aws.smus.isIamMode') && !getContext('aws.smus.inSmusSpaceEnvironment')) { + credentialsProvider = await smusAuthProvider.getCredentialsProviderForIamProfile( + (smusAuthProvider.activeConnection as SmusIamConnection).profileName + ) + } else { + credentialsProvider = await smusAuthProvider.getDerCredentialsProvider() + } + return DataZoneClient.createWithCredentials( + smusAuthProvider.getDomainRegion(), + smusAuthProvider.getDomainId(), + credentialsProvider + ) +} + +/** + * Creates a DataZoneClient with appropriate credentials provider for a specific project + * If domain mode is IAM mode, use the project credential provider + * If domain mode is not IAM mode, use the DER credential provider + * @param smusAuthProvider The SMUS authentication provider + * @param projectId The project ID for project-specific credentials + * @returns Promise resolving to DataZoneClient instance + */ +export async function createDZClientForProject( + smusAuthProvider: SmusAuthenticationProvider, + projectId: string +): Promise { + const credentialsProvider = getContext('aws.smus.isIamMode') + ? await smusAuthProvider.getProjectCredentialProvider(projectId) + : await smusAuthProvider.getDerCredentialsProvider() + + return DataZoneClient.createWithCredentials( + smusAuthProvider.getDomainRegion(), + smusAuthProvider.getDomainId(), + credentialsProvider + ) +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/client/README.md b/packages/core/src/sagemakerunifiedstudio/shared/client/README.md new file mode 100644 index 00000000000..17cc4767beb --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/README.md @@ -0,0 +1 @@ +# Common business logic and APIs for SageMaker Unified Studio features diff --git a/packages/core/src/sagemakerunifiedstudio/shared/client/connectionClientStore.ts b/packages/core/src/sagemakerunifiedstudio/shared/client/connectionClientStore.ts new file mode 100644 index 00000000000..e48f96f6bdb --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/connectionClientStore.ts @@ -0,0 +1,138 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { S3Client } from './s3Client' +import { SQLWorkbenchClient } from './sqlWorkbenchClient' +import { GlueClient } from './glueClient' +import { GlueCatalogClient } from './glueCatalogClient' +import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider' +import { ClientType } from '../../explorer/nodes/types' +import { S3ControlClient } from '@aws-sdk/client-s3-control' +import { getLogger } from '../../../shared/logger/logger' + +/** + * Client store for managing service clients per connection + */ +export class ConnectionClientStore { + private static instance: ConnectionClientStore + private clientCache: Record> = {} + + private constructor() {} + + public static getInstance(): ConnectionClientStore { + if (!ConnectionClientStore.instance) { + ConnectionClientStore.instance = new ConnectionClientStore() + } + return ConnectionClientStore.instance + } + + /** + * Gets or creates a client for a specific connection + */ + public getClient(connectionId: string, clientType: string, factory: () => T): T { + if (!this.clientCache[connectionId]) { + this.clientCache[connectionId] = {} + } + + if (!this.clientCache[connectionId][clientType]) { + this.clientCache[connectionId][clientType] = factory() + } + + return this.clientCache[connectionId][clientType] + } + + /** + * Gets or creates an S3Client for a connection + */ + public getS3Client( + connectionId: string, + region: string, + connectionCredentialsProvider: ConnectionCredentialsProvider + ): S3Client { + return this.getClient( + connectionId, + ClientType.S3Client, + () => new S3Client(region, connectionCredentialsProvider) + ) + } + + /** + * Gets or creates a SQLWorkbenchClient for a connection + */ + public getSQLWorkbenchClient( + connectionId: string, + region: string, + connectionCredentialsProvider: ConnectionCredentialsProvider + ): SQLWorkbenchClient { + return this.getClient(connectionId, ClientType.SQLWorkbenchClient, () => + SQLWorkbenchClient.createWithCredentials(region, connectionCredentialsProvider) + ) + } + + /** + * Gets or creates a GlueClient for a connection + */ + public getGlueClient( + connectionId: string, + region: string, + connectionCredentialsProvider: ConnectionCredentialsProvider + ): GlueClient { + return this.getClient( + connectionId, + ClientType.GlueClient, + () => new GlueClient(region, connectionCredentialsProvider) + ) + } + + /** + * Gets or creates a GlueCatalogClient for a connection + */ + public getGlueCatalogClient( + connectionId: string, + region: string, + connectionCredentialsProvider: ConnectionCredentialsProvider + ): GlueCatalogClient { + return this.getClient(connectionId, ClientType.GlueCatalogClient, () => + GlueCatalogClient.createWithCredentials(region, connectionCredentialsProvider) + ) + } + + /** + * Gets or creates an S3ControlClient for a connection + */ + public getS3ControlClient( + connectionId: string, + region: string, + connectionCredentialsProvider: ConnectionCredentialsProvider + ): S3ControlClient { + return this.getClient(connectionId, ClientType.S3ControlClient, () => { + const credentialsProvider = async () => { + const credentials = await connectionCredentialsProvider.getCredentials() + return { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.sessionToken, + expiration: credentials.expiration, + } + } + return new S3ControlClient({ region, credentials: credentialsProvider }) + }) + } + + /** + * Clears all cached clients for a connection + */ + public clearConnection(connectionId: string): void { + delete this.clientCache[connectionId] + } + + /** + * Clears all cached clients + */ + public clearAll(): void { + getLogger('smus').info('SMUS Connection: Clearing all cached clients') + this.clientCache = {} + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/client/credentialsAdapter.ts b/packages/core/src/sagemakerunifiedstudio/shared/client/credentialsAdapter.ts new file mode 100644 index 00000000000..ee8fef5a5d3 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/credentialsAdapter.ts @@ -0,0 +1,61 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as AWS from 'aws-sdk' +import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider' +import { getLogger } from '../../../shared/logger/logger' +import { CredentialsProvider } from '../../../auth/providers/credentials' + +/** + * Adapts a ConnectionCredentialsProvider (SDK v3) to work with SDK v2's CredentialProviderChain + */ +export function adaptConnectionCredentialsProvider( + connectionCredentialsProvider: ConnectionCredentialsProvider | CredentialsProvider +): AWS.CredentialProviderChain { + const provider = () => { + // Create SDK v2 Credentials that will resolve the provider when needed + const credentials = new AWS.Credentials({ + accessKeyId: '', + secretAccessKey: '', + sessionToken: '', + }) + + // Override the get method to use the connection credentials provider + credentials.get = (callback) => { + getLogger('smus').debug('Attempting to get credentials from ConnectionCredentialsProvider') + + connectionCredentialsProvider + .getCredentials() + .then((creds) => { + getLogger('smus').debug('Successfully got credentials') + + credentials.accessKeyId = creds.accessKeyId as string + credentials.secretAccessKey = creds.secretAccessKey as string + credentials.sessionToken = creds.sessionToken as string + credentials.expireTime = creds.expiration as Date + callback() + }) + .catch((err) => { + getLogger('smus').debug(`Failed to get credentials: ${err}`) + + callback(err) + }) + } + + // Override needsRefresh to delegate to the connection credentials provider + credentials.needsRefresh = () => { + return true // Always call refresh, this is okay because there is caching existing in credential provider + } + + // Override refresh to use the connection credentials provider + credentials.refresh = (callback) => { + credentials.get(callback) + } + + return credentials + } + + return new AWS.CredentialProviderChain([provider]) +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/client/datazoneClient.ts b/packages/core/src/sagemakerunifiedstudio/shared/client/datazoneClient.ts new file mode 100644 index 00000000000..be612ce459b --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/datazoneClient.ts @@ -0,0 +1,873 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ConnectionCredentials, + ConnectionSummary, + DataZone, + GetConnectionCommandOutput, + GetEnvironmentCredentialsCommandOutput, + ListConnectionsCommandOutput, + PhysicalEndpoint, + RedshiftPropertiesOutput, + S3PropertiesOutput, + ConnectionType, + GluePropertiesOutput, + GetEnvironmentCommandOutput, +} from '@aws-sdk/client-datazone' +import { getLogger } from '../../../shared/logger/logger' +import { DefaultStsClient } from '../../../shared/clients/stsClient' +import { getContext } from '../../../shared/vscode/setContext' +import { CredentialsProvider } from '../../../auth/providers/credentials' +import { DevSettings } from '../../../shared/settings' +import { ToolkitError } from '../../../shared/errors' +import { SmusErrorCodes } from '../smusUtils' + +/** + * Represents a DataZone domain + */ +export interface DataZoneDomain { + id: string + name: string + description?: string + status?: string + createdAt?: Date + updatedAt?: Date +} + +/** + * Represents a DataZone project + */ +export interface DataZoneProject { + id: string + name: string + description?: string + domainId: string + createdBy?: string + createdAt?: Date + updatedAt?: Date +} + +/** + * Represents JDBC connection properties + */ +export interface JdbcConnection { + jdbcIamUrl?: string + jdbcUrl?: string + username?: string + password?: string + secretId?: string + isProvisionedSecret?: boolean + redshiftTempDir?: string + host?: string + engine?: string + port?: number + dbname?: string + [key: string]: any +} + +/** + * Represents a DataZone connection + */ +export interface DataZoneConnection { + connectionId: string + name: string + description?: string + type: string + domainId: string + environmentId?: string + projectId: string + props?: { + s3Properties?: S3PropertiesOutput + redshiftProperties?: RedshiftPropertiesOutput + glueProperties?: GluePropertiesOutput + jdbcConnection?: JdbcConnection + [key: string]: any + } + /** + * Connection credentials when retrieved with withSecret=true + */ + connectionCredentials?: ConnectionCredentials + /** + * Location information parsed from physical endpoints + */ + location?: { + accessRole?: string + awsRegion?: string + awsAccountId?: string + iamConnectionId?: string + } + /** + * Glue connection name + */ + glueConnectionName?: string +} + +// Constants for DataZone environment configuration +const toolingBlueprintName = 'Tooling' +const sageMakerProviderName = 'Amazon SageMaker' + +/** + * Client for interacting with AWS DataZone API + * + * This client can be used with different credential providers + */ +export class DataZoneClient { + private datazoneClient: DataZone | undefined + private static instances = new Map() + private readonly logger = getLogger('smus') + + private constructor( + private readonly region: string, + private readonly domainId: string, + private readonly credentialsProvider?: CredentialsProvider + ) {} + + /** + * Creates a new DataZoneClient instance with specific credentials + * @param region AWS region + * @param domainId DataZone domain ID + * @param credentialsProvider Credentials provider + * @returns DataZoneClient instance with credentials + */ + public static createWithCredentials( + region: string, + domainId: string, + credentialsProvider: CredentialsProvider + ): DataZoneClient { + const instanceKey = credentialsProvider.getHashCode() + + if (DataZoneClient.instances.has(instanceKey)) { + const existingInstance = DataZoneClient.instances.get(instanceKey)! + getLogger('smus').debug(`DataZoneClient: Using existing instance, instance key is ${instanceKey}`) + return existingInstance + } + + // Create new instance + getLogger('smus').debug(`DataZoneClient: Creating new instance with instance key ${instanceKey}`) + const instance = new DataZoneClient(region, domainId, credentialsProvider) + DataZoneClient.instances.set(instanceKey, instance) + + return instance + } + + /** + * Disposes all cached DataZoneClient instances + */ + public static dispose(): void { + const logger = getLogger('smus') + getLogger('smus').debug('DataZoneClient: Disposing all cached instances') + + for (const [key, instance] of DataZoneClient.instances.entries()) { + instance.datazoneClient = undefined + logger.debug(`DataZoneClient: Disposed instance for: ${key}`) + } + + DataZoneClient.instances.clear() + } + + /** + * Parse a Redshift connection info object from JDBC URL + * @param jdbcURL Example JDBC URL: jdbc:redshift://redshift-serverless-workgroup-3zzw0fjmccdixz.123456789012.us-east-1.redshift-serverless.amazonaws.com:5439/dev + * @returns A object contains info of host, engine, port, dbName + */ + private getRedshiftConnectionInfoFromJdbcURL(jdbcURL: string) { + if (!jdbcURL) { + return + } + + const [, engine, hostWithLeadingSlashes, portAndDBName] = jdbcURL.split(':') + const [port, dbName] = portAndDBName.split('/') + return { + host: hostWithLeadingSlashes.split('/')[2], + engine, + port, + dbName, + } + } + + /** + * Builds a JDBC connection object from Redshift properties + * @param redshiftProps The Redshift properties + * @returns A JDBC connection object + */ + private buildJdbcConnectionFromRedshiftProps(redshiftProps: RedshiftPropertiesOutput): JdbcConnection { + const redshiftConnectionInfo = this.getRedshiftConnectionInfoFromJdbcURL(redshiftProps.jdbcUrl ?? '') + + return { + jdbcIamUrl: redshiftProps.jdbcIamUrl, + jdbcUrl: redshiftProps.jdbcUrl, + username: redshiftProps.credentials?.usernamePassword?.username, + password: redshiftProps.credentials?.usernamePassword?.password, + secretId: redshiftProps.credentials?.secretArn, + isProvisionedSecret: redshiftProps.isProvisionedSecret, + redshiftTempDir: redshiftProps.redshiftTempDir, + host: redshiftConnectionInfo?.host, + engine: redshiftConnectionInfo?.engine, + port: Number(redshiftConnectionInfo?.port), + dbname: redshiftConnectionInfo?.dbName, + } + } + + /** + * Gets the DataZone domain ID + * @returns DataZone domain ID + */ + public getDomainId(): string { + return this.domainId + } + + /** + * Gets the AWS region + * @returns AWS region + */ + public getRegion(): string { + return this.region + } + + /** + * Gets the default tooling environment credentials for a DataZone project + * @param projectId The DataZone project identifier + * @returns Promise resolving to environment credentials + * @throws Error if tooling blueprint or environment is not found + */ + public async getProjectDefaultEnvironmentCreds(projectId: string): Promise { + try { + this.logger.debug( + `Getting project default environment credentials for domain ${this.domainId}, project ${projectId}` + ) + const datazoneClient = await this.getDataZoneClient() + + this.logger.debug('Listing environment blueprints') + const domainBlueprints = await datazoneClient.listEnvironmentBlueprints({ + domainIdentifier: this.domainId, + managed: true, + name: this.getToolingBlueprintName(), + }) + const toolingBlueprint = domainBlueprints.items?.[0] + if (!toolingBlueprint) { + this.logger.error('Failed to get tooling blueprint') + throw new Error('Failed to get tooling blueprint') + } + this.logger.debug(`Found tooling blueprint with ID: ${toolingBlueprint.id}, listing environments`) + + const listEnvs = await datazoneClient.listEnvironments({ + domainIdentifier: this.domainId, + projectIdentifier: projectId, + environmentBlueprintIdentifier: toolingBlueprint.id, + provider: sageMakerProviderName, + }) + + const defaultEnv = listEnvs.items?.[0] + if (!defaultEnv) { + this.logger.error('Failed to find default Tooling environment') + throw new Error('Failed to find default Tooling environment') + } + this.logger.debug(`Found default environment with ID: ${defaultEnv.id}, getting environment credentials`) + + const defaultEnvCreds = await datazoneClient.getEnvironmentCredentials({ + domainIdentifier: this.domainId, + environmentIdentifier: defaultEnv.id, + }) + + return defaultEnvCreds + } catch (err) { + this.logger.error('Failed to get project default environment credentials: %s', err as Error) + throw err + } + } + + /** + * Gets the DataZone client, initializing it if necessary + */ + private async getDataZoneClient(): Promise { + if (!this.datazoneClient) { + try { + if (this.credentialsProvider) { + const awsCredentialProvider = async () => { + const credentials = await this.credentialsProvider!.getCredentials() + return { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.sessionToken, + expiration: credentials.expiration, + } + } + + const clientConfig: any = { + region: this.region, + credentials: awsCredentialProvider, + } + + // Use user setting for endpoint if provided + const devSettings = DevSettings.instance + const customEndpoint = devSettings.get('endpoints', {})['datazone'] + if (customEndpoint) { + clientConfig.endpoint = customEndpoint + this.logger.debug( + `DataZoneClient: Using custom DataZone endpoint from settings: ${customEndpoint}` + ) + } + + this.datazoneClient = new DataZone(clientConfig) + } else { + throw new Error('No credentials provider provided') + } + + this.logger.info('DataZoneClient: Successfully created authenticated DataZone client') + } catch (err) { + this.logger.error('DataZoneClient: Failed to create DataZone client: %s', err as Error) + throw err + } + } + return this.datazoneClient + } + + /** + * Lists project memberships in a DataZone project with pagination support + * @param options Options for listing project memberships + * @returns Paginated list of DataZone project permissions with nextToken + */ + public async listProjectMemberships(options: { + projectIdentifier: string + maxResults?: number + nextToken?: string + }): Promise<{ memberships: any[]; nextToken?: string }> { + try { + this.logger.info( + `DataZoneClient: Listing project memberships for project ${options.projectIdentifier} in domain ${this.domainId}` + ) + + const datazoneClient = await this.getDataZoneClient() + + const response = await datazoneClient.listProjectMemberships({ + domainIdentifier: this.domainId, + projectIdentifier: options.projectIdentifier, + maxResults: options.maxResults, + nextToken: options.nextToken, + }) + + if (!response.members || response.members.length === 0) { + this.logger.info( + `DataZoneClient: No project memberships found for project ${options.projectIdentifier}` + ) + return { memberships: [] } + } + + this.logger.debug( + `DataZoneClient: Found ${response.members.length} project memberships for project ${options.projectIdentifier}` + ) + return { memberships: response.members, nextToken: response.nextToken } + } catch (err) { + this.logger.error('DataZoneClient: Failed to list project memberships: %s', (err as Error).message) + throw err + } + } + + /** + * Fetches all project memberships in a DataZone project by handling pagination automatically + * @param projectIdentifier The DataZone project identifier + * @returns Promise resolving to an array of all project memberships + */ + public async fetchAllProjectMemberships(projectIdentifier: string): Promise { + try { + let allMemberships: any[] = [] + let nextToken: string | undefined + do { + const maxResultsPerPage = 50 + const response = await this.listProjectMemberships({ + projectIdentifier, + nextToken, + maxResults: maxResultsPerPage, + }) + allMemberships = [...allMemberships, ...response.memberships] + nextToken = response.nextToken + } while (nextToken) + + this.logger.debug(`DataZoneClient: Fetched a total of ${allMemberships.length} project memberships`) + return allMemberships + } catch (err) { + this.logger.error('DataZoneClient: Failed to fetch all project memberships: %s', (err as Error).message) + throw err + } + } + + /** + * Lists projects in a DataZone domain with pagination support + * @param options Options for listing projects + * @returns Paginated list of DataZone projects with nextToken + */ + public async listProjects(options?: { + maxResults?: number + userIdentifier?: string + groupIdentifier?: string + name?: string + nextToken?: string + }): Promise<{ projects: DataZoneProject[]; nextToken?: string }> { + try { + this.logger.info(`DataZoneClient: Listing projects for domain ${this.domainId} in region ${this.region}`) + + const datazoneClient = await this.getDataZoneClient() + + // Call the DataZone API to list projects with pagination + const response = await datazoneClient.listProjects({ + domainIdentifier: this.domainId, + maxResults: options?.maxResults, + userIdentifier: options?.userIdentifier, + groupIdentifier: options?.groupIdentifier, + name: options?.name, + nextToken: options?.nextToken, + }) + + if (!response.items || response.items.length === 0) { + this.logger.info(`DataZoneClient: No projects found for domain ${this.domainId}`) + return { projects: [] } + } + + // Map the response to our DataZoneProject interface + const projects: DataZoneProject[] = response.items.map((project) => ({ + id: project.id || '', + name: project.name || '', + description: project.description, + domainId: this.domainId, + createdBy: project.createdBy, + createdAt: project.createdAt ? new Date(project.createdAt) : undefined, + updatedAt: project.updatedAt ? new Date(project.updatedAt) : undefined, + })) + + this.logger.debug(`DataZoneClient: Found ${projects.length} projects for domain ${this.domainId}`) + return { projects, nextToken: response.nextToken } + } catch (err) { + this.logger.error('DataZoneClient: Failed to list projects: %s', (err as Error).message) + throw err + } + } + + /** + * Fetches all projects in a DataZone domain by handling pagination automatically + * @param options Options for listing projects (excluding nextToken which is handled internally) + * @returns Promise resolving to an array of all DataZone projects + */ + public async fetchAllProjects(options?: { + userIdentifier?: string + groupIdentifier?: string + name?: string + }): Promise { + try { + let allProjects: DataZoneProject[] = [] + let nextToken: string | undefined + do { + const maxResultsPerPage = 50 + const response = await this.listProjects({ + ...options, + nextToken, + maxResults: maxResultsPerPage, + }) + allProjects = [...allProjects, ...response.projects] + nextToken = response.nextToken + } while (nextToken) + + this.logger.debug(`DataZoneClient: Fetched a total of ${allProjects.length} projects`) + return allProjects + } catch (err) { + this.logger.error('DataZoneClient: Failed to fetch all projects: %s', (err as Error).message) + throw err + } + } + + /** + * Gets a specific project by ID + * @param projectId The project identifier + * @returns Promise resolving to the project details + */ + public async getProject(projectId: string): Promise { + try { + this.logger.info(`DataZoneClient: Getting project ${projectId} in domain ${this.domainId}`) + + const datazoneClient = await this.getDataZoneClient() + + const response = await datazoneClient.getProject({ + domainIdentifier: this.domainId, + identifier: projectId, + }) + + const project: DataZoneProject = { + id: response.id || '', + name: response.name || '', + description: response.description, + domainId: this.domainId, + createdAt: response.createdAt ? new Date(response.createdAt) : undefined, + updatedAt: response.lastUpdatedAt ? new Date(response.lastUpdatedAt) : undefined, + } + + this.logger.debug(`DataZoneClient: Retrieved project ${projectId} with name: ${project.name}`) + return project + } catch (err) { + this.logger.error('DataZoneClient: Failed to get project: %s', err as Error) + throw err + } + } + + /* + * Processes a connection response to add jdbcConnection if it's a Redshift connection + * @param connection The connection object to process + * @param connectionType The connection type + */ + private processRedshiftConnection(connection: ConnectionSummary): void { + if ( + connection && + connection.props && + 'redshiftProperties' in connection.props && + connection.props.redshiftProperties && + connection.type?.toLowerCase().includes('redshift') + ) { + const redshiftProps = connection.props.redshiftProperties as RedshiftPropertiesOutput + const props = connection.props as Record + + if (!props.jdbcConnection) { + props.jdbcConnection = this.buildJdbcConnectionFromRedshiftProps(redshiftProps) + } + } + } + + /** + * Parses location from physical endpoints + * @param physicalEndpoints Array of physical endpoints + * @returns Location object or undefined + */ + private parseLocationFromPhysicalEndpoints(physicalEndpoints?: PhysicalEndpoint[]): DataZoneConnection['location'] { + if (physicalEndpoints && physicalEndpoints.length > 0) { + const physicalEndpoint = physicalEndpoints[0] + return { + accessRole: physicalEndpoint.awsLocation?.accessRole, + awsRegion: physicalEndpoint.awsLocation?.awsRegion, + awsAccountId: physicalEndpoint.awsLocation?.awsAccountId, + iamConnectionId: physicalEndpoint.awsLocation?.iamConnectionId, + } + } + return undefined + } + + /** + * Parses glueConnectionName from physical endpoints + * @param physicalEndpoints Array of physical endpoints + * @returns glueConnectionName or undefined + */ + // eslint-disable-next-line id-length + private parseGlueConnectionNameFromPhysicalEndpoints( + physicalEndpoints?: PhysicalEndpoint[] + ): DataZoneConnection['glueConnectionName'] { + if (physicalEndpoints && physicalEndpoints.length > 0) { + const physicalEndpoint = physicalEndpoints[0] + return physicalEndpoint.glueConnectionName + } + return undefined + } + + /** + * Gets a specific connection by ID + * @param params Parameters for getting a connection + * @returns The connection details + */ + public async getConnection(params: { + domainIdentifier: string + identifier: string + withSecret?: boolean + }): Promise { + try { + this.logger.info( + `DataZoneClient: Getting connection ${params.identifier} in domain ${params.domainIdentifier}` + ) + + const datazoneClient = await this.getDataZoneClient() + + // Call the DataZone API to get connection + const response: GetConnectionCommandOutput = await datazoneClient.getConnection({ + domainIdentifier: params.domainIdentifier, + identifier: params.identifier, + withSecret: params.withSecret !== undefined ? params.withSecret : true, + }) + + // Process the connection to add jdbcConnection if it's a Redshift connection + this.processRedshiftConnection(response) + + // Parse location from physical endpoints + const location = this.parseLocationFromPhysicalEndpoints(response.physicalEndpoints) + + const glueConnectionName = this.parseGlueConnectionNameFromPhysicalEndpoints(response.physicalEndpoints) + + // Return as DataZoneConnection, currently only required fields are added + // Can always include new fields in DataZoneConnection when needed + const connection: DataZoneConnection = { + connectionId: response.connectionId || '', + name: response.name || '', + description: response.description, + type: response.type || '', + domainId: params.domainIdentifier, + projectId: response.projectId || '', + props: response.props || {}, + connectionCredentials: response.connectionCredentials, + location, + glueConnectionName, + } + + return connection + } catch (err) { + this.logger.error('DataZoneClient: Failed to get connection: %s', err as Error) + throw err + } + } + + public async fetchConnections( + domain: string | undefined, + project: string | undefined, + ConnectionType: ConnectionType + ): Promise { + const datazoneClient = await this.getDataZoneClient() + return datazoneClient.listConnections({ + domainIdentifier: domain, + projectIdentifier: project, + type: ConnectionType, + }) + } + /** + * Lists connections in a DataZone environment + * @param domainId The DataZone domain identifier + * @param environmentId The DataZone environment identifier + * @param projectId The DataZone project identifier + * @returns List of DataZone connections + */ + public async listConnections( + domainId: string, + environmentId: string | undefined, + projectId: string + ): Promise { + try { + this.logger.info( + `DataZoneClient: Listing connections for environment ${environmentId} in domain ${domainId}` + ) + + const datazoneClient = await this.getDataZoneClient() + let allConnections: DataZoneConnection[] = [] + let nextToken: string | undefined + + do { + // Call the DataZone API to list connections with pagination + const response: ListConnectionsCommandOutput = await datazoneClient.listConnections({ + domainIdentifier: domainId, + projectIdentifier: projectId, + environmentIdentifier: environmentId, + nextToken, + maxResults: 50, + }) + + if (response.items && response.items.length > 0) { + // Map the response to our DataZoneConnection interface + const connections: DataZoneConnection[] = response.items.map((connection) => { + // Process the connection to add jdbcConnection if it's a Redshift connection + this.processRedshiftConnection(connection) + + // Parse location from physical endpoints + const location = this.parseLocationFromPhysicalEndpoints(connection.physicalEndpoints) + + const glueConnectionName = this.parseGlueConnectionNameFromPhysicalEndpoints( + connection.physicalEndpoints + ) + + return { + connectionId: connection.connectionId || '', + name: connection.name || '', + description: '', + type: connection.type || '', + domainId, + environmentId, + projectId, + props: connection.props || {}, + location, + glueConnectionName, + } + }) + allConnections = [...allConnections, ...connections] + } + + nextToken = response.nextToken + } while (nextToken) + + this.logger.info(`DataZoneClient: Fetched a total of ${allConnections.length} connections`) + return allConnections + } catch (err) { + this.logger.error('DataZoneClient: Failed to list connections: %s', err as Error) + throw err + } + } + + /** + * Gets the tooling environment ID for a project + * @param domainId The DataZone domain identifier + * @param projectId The DataZone project identifier + * @returns Promise resolving to the tooling environment ID + */ + public async getToolingEnvironmentId(domainId: string, projectId: string): Promise { + this.logger.debug(`Getting tooling environment ID for domain ${domainId}, project ${projectId}`) + const datazoneClient = await this.getDataZoneClient() + + let domainBlueprints + try { + // Get the tooling blueprint + domainBlueprints = await datazoneClient.listEnvironmentBlueprints({ + domainIdentifier: domainId, + managed: true, + name: this.getToolingBlueprintName(), + }) + } catch (err) { + this.logger.error( + 'Failed to list environment blueprints for domain %s, %s', + domainId, + (err as Error).message + ) + throw err + } + + const toolingBlueprint = domainBlueprints.items?.[0] + if (!toolingBlueprint) { + this.logger.error('No tooling blueprint found for domain %s', domainId) + throw new Error('No tooling blueprint found') + } + + // List environments for the project + let listEnvs + try { + this.logger.debug(`Listing environments for project ${projectId} with blueprint ${toolingBlueprint.id}`) + listEnvs = await datazoneClient.listEnvironments({ + domainIdentifier: domainId, + projectIdentifier: projectId, + environmentBlueprintIdentifier: toolingBlueprint.id, + provider: sageMakerProviderName, + }) + } catch (err) { + this.logger.error( + 'Failed to list environments for domainId: %s, projectId: %s, %s', + domainId, + projectId, + (err as Error).message + ) + throw err + } + + const defaultEnv = listEnvs.items?.[0] + if (!defaultEnv || !defaultEnv.id) { + this.logger.error( + 'No default Tooling environment found for domainId: %s, projectId: %s', + domainId, + projectId + ) + throw new Error('No default Tooling environment found for project') + } + this.logger.debug(`Found tooling environment with ID: ${defaultEnv.id}`) + return defaultEnv.id + } + + /** + * Gets environment details + * @param environmentId The environment identifier + * @returns Promise resolving to environment details + */ + public async getEnvironmentDetails( + environmentId: string + ): Promise { + try { + this.logger.debug( + `Getting environment details for domain ${this.getDomainId()}, environment ${environmentId}` + ) + const datazoneClient = await this.getDataZoneClient() + + const environment = await datazoneClient.getEnvironment({ + domainIdentifier: this.getDomainId(), + identifier: environmentId, + }) + + this.logger.debug(`Retrieved environment details for ${environmentId}`) + return environment + } catch (err) { + this.logger.error('Failed to get environment details: %s', err as Error) + throw err + } + } + + /** + * Gets the tooling environment details for a project + * @param projectId The project ID + * @returns The tooling environment details + */ + public async getToolingEnvironment(projectId: string): Promise { + const toolingEnvId = await this.getToolingEnvironmentId(this.getDomainId(), projectId) + if (!toolingEnvId) { + throw new Error('No default environment found for project') + } + return await this.getEnvironmentDetails(toolingEnvId) + } + + public async getUserId(): Promise { + if (!this.credentialsProvider) { + throw new Error('Credentials provider is required for getUserId') + } + const callerCredentials = await this.credentialsProvider.getCredentials() + const stsClient = new DefaultStsClient(this.getRegion(), callerCredentials) + const callerIdentity = await stsClient.getCallerIdentity() + this.logger.debug(`Retrieved caller identity, UserId: ${callerIdentity.UserId}`) + return callerIdentity.UserId + } + + /** + * Gets the user profile ID for a given IAM principal + * @param userIdentifier IAM user or role ARN + * @param domainIdentifier Optional domain identifier. If not provided, uses the client's domain ID + * @returns Promise resolving to the user profile ID + * @throws ToolkitError with appropriate error code + */ + public async getUserProfileIdForIamPrincipal( + userIdentifier: string, + domainIdentifier?: string + ): Promise { + try { + this.logger.debug(`DataZoneClient: Getting user profile for IAM ARN: ${userIdentifier}`) + + const datazoneClient = await this.getDataZoneClient() + + const params = { + domainIdentifier: domainIdentifier || this.getDomainId(), + userIdentifier: userIdentifier, + } + + const userProfile = await datazoneClient.getUserProfile(params) + + if (!userProfile.id) { + this.logger.error(`DataZoneClient: No user profile ID returned for ARN: ${userIdentifier}`) + throw new ToolkitError(`No user profile found for IAM principal: ${userIdentifier}`, { + code: SmusErrorCodes.NoUserProfileFound, + }) + } + + this.logger.debug(`DataZoneClient: Retrieved user profile ID: ${userProfile.id}`) + return userProfile.id + } catch (err) { + // Re-throw if it's already a ToolkitError + if (err instanceof ToolkitError) { + throw err + } + + // Log and wrap other errors + this.logger.error('DataZoneClient: Failed to get user profile ID: %s', (err as Error).message) + throw ToolkitError.chain(err, 'Failed to get user profile ID') + } + } + + /** + * Gets the correct tooling blueprint name + */ + private getToolingBlueprintName(): string { + return getContext('aws.smus.isIamMode') ? 'ToolingLite' : toolingBlueprintName + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/client/datazoneCustomClientHelper.ts b/packages/core/src/sagemakerunifiedstudio/shared/client/datazoneCustomClientHelper.ts new file mode 100644 index 00000000000..684bddf2e08 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/datazoneCustomClientHelper.ts @@ -0,0 +1,525 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLogger } from '../../../shared/logger/logger' +import apiConfig = require('./datazonecustomclient.json') +import globals from '../../../shared/extensionGlobals' +import { Service } from 'aws-sdk' +import { ServiceConfigurationOptions } from 'aws-sdk/lib/service' +import * as DataZoneCustomClient from './datazonecustomclient' +import { adaptConnectionCredentialsProvider } from './credentialsAdapter' +import { CredentialsProvider } from '../../../auth/providers/credentials' +import { ToolkitError } from '../../../shared/errors' +import { SmusUtils, isIamDomain } from '../smusUtils' +import { DevSettings } from '../../../shared/settings' + +import { SmusErrorCodes } from '../smusUtils' + +/** + * Error codes for DataZone operations + * @deprecated Use SmusErrorCodes instead + */ +export const DataZoneErrorCode = { + NoGroupProfileFound: SmusErrorCodes.NoGroupProfileFound, + NoUserProfileFound: SmusErrorCodes.NoUserProfileFound, +} as const + +/** + * Helper client for interacting with AWS DataZone Custom API + */ +export class DataZoneCustomClientHelper { + private datazoneCustomClient: DataZoneCustomClient | undefined + private static instances = new Map() + private readonly logger = getLogger('smus') + + private constructor( + private readonly credentialProvider: CredentialsProvider, + private readonly region: string + ) {} + + /** + * Gets a singleton instance of the DataZoneCustomClientHelper + * @returns DataZoneCustomClientHelper instance + */ + public static getInstance(credentialProvider: CredentialsProvider, region: string): DataZoneCustomClientHelper { + const logger = getLogger('smus') + + const instanceKey = `${region}` + + // Check if we already have an instance for this instanceKey + if (DataZoneCustomClientHelper.instances.has(instanceKey)) { + const existingInstance = DataZoneCustomClientHelper.instances.get(instanceKey)! + logger.debug(`DataZoneCustomClientHelper: Using existing instance for instanceKey ${instanceKey}`) + return existingInstance + } + + // Create new instance + logger.debug('DataZoneCustomClientHelper: Creating new instance') + const instance = new DataZoneCustomClientHelper(credentialProvider, region) + DataZoneCustomClientHelper.instances.set(instanceKey, instance) + + logger.debug(`DataZoneCustomClientHelper: Created instance with instanceKey ${instanceKey}`) + + return instance + } + + /** + * Disposes all instances and cleans up resources + */ + public static dispose(): void { + const logger = getLogger('smus') + logger.debug('DataZoneCustomClientHelper: Disposing all instances') + + for (const [key, instance] of DataZoneCustomClientHelper.instances.entries()) { + instance.datazoneCustomClient = undefined + logger.debug(`DataZoneCustomClientHelper: Disposed instance for: ${key}`) + } + + DataZoneCustomClientHelper.instances.clear() + } + + /** + * Gets the AWS region + * @returns AWS region + */ + public getRegion(): string { + return this.region + } + + /** + * Gets the DataZone client, initializing it if necessary + */ + private async getDataZoneCustomClient(): Promise { + if (!this.datazoneCustomClient) { + try { + this.logger.info('DataZoneCustomClientHelper: Creating authenticated DataZone client') + + // Use user setting for endpoint if provided, otherwise use default + const devSettings = DevSettings.instance + const customEndpoint = devSettings.get('endpoints', {})['datazone'] + const endpoint = customEndpoint || `https://datazone.${this.region}.api.aws` + + if (customEndpoint) { + this.logger.debug( + `DataZoneCustomClientHelper: Using custom DataZone endpoint from settings: ${endpoint}` + ) + } + + this.datazoneCustomClient = (await globals.sdkClientBuilder.createAwsService( + Service, + { + apiConfig: apiConfig, + endpoint: endpoint, + region: this.region, + credentialProvider: adaptConnectionCredentialsProvider(this.credentialProvider), + } as ServiceConfigurationOptions, + undefined, + false + )) as DataZoneCustomClient + + this.logger.info('DataZoneCustomClientHelper: Successfully created authenticated DataZone client') + } catch (err) { + this.logger.error('DataZoneCustomClientHelper: Failed to create DataZone client: %s', err as Error) + throw err + } + } + return this.datazoneCustomClient + } + + /** + * Lists domains in DataZone with pagination support + * @param options Options for listing domains + * @returns Paginated list of DataZone domains with nextToken + */ + public async listDomains(options?: { + maxResults?: number + status?: string + nextToken?: string + }): Promise<{ domains: DataZoneCustomClient.Types.DomainSummary[]; nextToken?: string }> { + try { + this.logger.info(`DataZoneCustomClientHelper: Listing domains in region ${this.region}`) + + const datazoneCustomClient = await this.getDataZoneCustomClient() + + // Call DataZone API to list domains with pagination + const response = await datazoneCustomClient + .listDomains({ + maxResults: options?.maxResults, + status: options?.status, + nextToken: options?.nextToken, + }) + .promise() + + const domains = response.items || [] + + if (domains.length === 0) { + this.logger.info(`DataZoneCustomClientHelper: No domains found`) + } else { + this.logger.debug(`DataZoneCustomClientHelper: Found ${domains.length} domains`) + } + + return { domains, nextToken: response.nextToken } + } catch (err) { + this.logger.error('DataZoneCustomClientHelper: Failed to list domains: %s', (err as Error).message) + throw err + } + } + + /** + * Fetches all domains by handling pagination automatically + * @param options Options for listing domains (excluding nextToken which is handled internally) + * @returns Promise resolving to an array of all DataZone domains + */ + public async fetchAllDomains(options?: { status?: string }): Promise { + try { + let allDomains: DataZoneCustomClient.Types.DomainSummary[] = [] + let nextToken: string | undefined + do { + const maxResultsPerPage = 25 + const response = await this.listDomains({ + ...options, + nextToken, + maxResults: maxResultsPerPage, + }) + allDomains = [...allDomains, ...response.domains] + nextToken = response.nextToken + } while (nextToken) + + this.logger.debug(`DataZoneCustomClientHelper: Fetched a total of ${allDomains.length} domains`) + return allDomains + } catch (err) { + this.logger.error('DataZoneCustomClientHelper: Failed to fetch all domains: %s', (err as Error).message) + throw err + } + } + + /** + * Gets the domain with IAM authentication mode using pagination with early termination + * @returns Promise resolving to the DataZone domain or undefined if not found + */ + public async getIamDomain(): Promise { + const logger = getLogger('smus') + + try { + logger.info('DataZoneCustomClientHelper: Getting the domain info') + + let nextToken: string | undefined + let totalDomainsChecked = 0 + const maxResultsPerPage = 25 + + // Paginate through domains and check each page for IAM domain + do { + const response = await this.listDomains({ + status: 'AVAILABLE', + nextToken, + maxResults: maxResultsPerPage, + }) + + const { domains } = response + totalDomainsChecked += domains.length + + logger.debug( + `DataZoneCustomClientHelper: Checking ${domains.length} domains in current page (total checked: ${totalDomainsChecked})` + ) + + // Check each domain in the current page for IAM domain + for (const domain of domains) { + // Log the complete domain object for debugging + logger.debug(`DataZoneCustomClientHelper: Domain ${domain.id} full response: %O`, domain) + + const isIam = isIamDomain({ + domainVersion: domain.domainVersion, + iamSignIns: domain.iamSignIns, + domainId: domain.id, + }) + + if (isIam) { + logger.info(`DataZoneCustomClientHelper: Found IAM domain, id: ${domain.id} (${domain.name})`) + return domain + } + } + + nextToken = response.nextToken + } while (nextToken) + + logger.info( + `DataZoneCustomClientHelper: No IAM domain found after checking all ${totalDomainsChecked} domains` + ) + return undefined + } catch (err) { + logger.error('DataZoneCustomClientHelper: Failed to get domain info: %s', err as Error) + throw new Error(`Failed to get domain info: ${(err as Error).message}`) + } + } + + /** + * Gets a specific domain by its ID + * @param domainId The ID of the domain to retrieve + * @returns Promise resolving to the GetDomainOutput + */ + public async getDomain(domainId: string): Promise { + try { + this.logger.debug(`DataZoneCustomClientHelper: Getting domain with ID: ${domainId}`) + + const datazoneCustomClient = await this.getDataZoneCustomClient() + + const response = await datazoneCustomClient + .getDomain({ + identifier: domainId, + }) + .promise() + + this.logger.debug(`DataZoneCustomClientHelper: Successfully retrieved domain: ${domainId}`) + return response + } catch (err) { + this.logger.error('DataZoneCustomClientHelper: Failed to get domain: %s', (err as Error).message) + throw err + } + } + + /** + * Searches for group profiles in the DataZone domain + * @param domainIdentifier The domain identifier to search in + * @param options Options for searching group profiles + * @returns Promise resolving to group profile search results with pagination + */ + public async searchGroupProfiles( + domainIdentifier: string, + options?: { + groupType?: string + searchText?: string + maxResults?: number + nextToken?: string + } + ): Promise<{ items: DataZoneCustomClient.Types.GroupProfileSummary[]; nextToken?: string }> { + try { + this.logger.debug( + `DataZoneCustomClientHelper: Searching group profiles in domain ${domainIdentifier} with groupType: ${options?.groupType}, searchText: ${options?.searchText}` + ) + + const datazoneCustomClient = await this.getDataZoneCustomClient() + + // Build the request parameters + const params: DataZoneCustomClient.Types.SearchGroupProfilesInput = { + domainIdentifier, + groupType: options?.groupType as DataZoneCustomClient.Types.GroupSearchType, + searchText: options?.searchText, + maxResults: options?.maxResults, + nextToken: options?.nextToken, + } + + // Call DataZone API to search group profiles + const response = await datazoneCustomClient.searchGroupProfiles(params).promise() + + const items = response.items || [] + + if (items.length === 0) { + this.logger.debug(`DataZoneCustomClientHelper: No group profiles found`) + } else { + this.logger.debug(`DataZoneCustomClientHelper: Found ${items.length} group profiles`) + } + + return { items, nextToken: response.nextToken } + } catch (err) { + this.logger.error('DataZoneCustomClientHelper: Failed to search group profiles: %s', (err as Error).message) + throw err + } + } + + /** + * Searches for user profiles in the DataZone domain + * @param domainIdentifier The domain identifier to search in + * @param options Options for searching user profiles + * @returns Promise resolving to user profile search results with pagination + */ + public async searchUserProfiles( + domainIdentifier: string, + options: { + userType: string + searchText?: string + maxResults?: number + nextToken?: string + } + ): Promise<{ items: DataZoneCustomClient.Types.UserProfileSummary[]; nextToken?: string }> { + try { + this.logger.debug( + `DataZoneCustomClientHelper: Searching user profiles in domain ${domainIdentifier} with userType: ${options.userType}, searchText: ${options.searchText}` + ) + + const datazoneCustomClient = await this.getDataZoneCustomClient() + + // Build the request parameters + const params: DataZoneCustomClient.Types.SearchUserProfilesInput = { + domainIdentifier, + userType: options.userType as DataZoneCustomClient.Types.UserSearchType, + searchText: options.searchText, + maxResults: options.maxResults, + nextToken: options.nextToken, + } + + // Call DataZone API to search user profiles + const response = await datazoneCustomClient.searchUserProfiles(params).promise() + + const items = response.items || [] + + if (items.length === 0) { + this.logger.debug(`DataZoneCustomClientHelper: No user profiles found`) + } else { + this.logger.debug(`DataZoneCustomClientHelper: Found ${items.length} user profiles`) + } + + return { items, nextToken: response.nextToken } + } catch (err) { + this.logger.error('DataZoneCustomClientHelper: Failed to search user profiles: %s', (err as Error).message) + throw err + } + } + + /** + * Gets the group profile ID for a given IAM role ARN + * @param domainIdentifier The domain identifier to search in + * @param roleArn The base IAM role ARN (format: arn:aws:iam::ACCOUNT:role/ROLE_NAME) + * @returns Promise resolving to the group profile ID + * @throws ToolkitError with appropriate error code + */ + public async getGroupProfileId(domainIdentifier: string, roleArn: string): Promise { + try { + this.logger.debug( + `DataZoneCustomClientHelper: Getting group profile ID for role ARN: ${roleArn} in domain ${domainIdentifier}` + ) + + // Use searchText to filter server-side for better performance + const response = await this.searchGroupProfiles(domainIdentifier, { + groupType: 'IAM_ROLE_SESSION_GROUP', + searchText: roleArn, + maxResults: 50, + }) + + this.logger.debug( + `DataZoneCustomClientHelper: Received ${response.items.length} group profiles from search` + ) + + // Find exact match in filtered results + for (const profile of response.items) { + this.logger.debug( + `DataZoneCustomClientHelper: Checking group profile - ID: ${profile.id}, rolePrincipalArn: ${profile.rolePrincipalArn}, status: ${profile.status}` + ) + + if (profile.rolePrincipalArn === roleArn) { + this.logger.info(`DataZoneCustomClientHelper: Found matching group profile with ID: ${profile.id}`) + return profile.id! + } + } + + // No matching profile found + this.logger.error(`DataZoneCustomClientHelper: No group profile found for IAM role: ${roleArn}`) + throw new ToolkitError(`No group profile found for IAM role: ${roleArn}`, { + code: SmusErrorCodes.NoGroupProfileFound, + }) + } catch (err) { + // Re-throw if it's already a ToolkitError + if (err instanceof ToolkitError) { + throw err + } + + // Log and wrap other errors + this.logger.error('DataZoneCustomClientHelper: Failed to get group profile ID: %s', (err as Error).message) + throw ToolkitError.chain(err, 'Failed to get group profile ID') + } + } + + /** + * Gets the user profile ID for a given IAM role session + * @param domainIdentifier The domain identifier to search in + * @param roleArnWithSession The assumed role ARN with session name (format: arn:aws:sts::ACCOUNT:assumed-role/ROLE_NAME/SESSION_NAME) + * @returns Promise resolving to the user profile ID + * @throws ToolkitError with appropriate error code + */ + public async getUserProfileIdForSession(domainIdentifier: string, roleArnWithSession: string): Promise { + try { + this.logger.debug( + `DataZoneCustomClientHelper: Getting user profile ID for role ARN with session: ${roleArnWithSession} in domain ${domainIdentifier}` + ) + + // Extract session name from the assumed role ARN + // Format: arn:aws:sts::ACCOUNT:assumed-role/ROLE_NAME/SESSION_NAME + const sessionName = SmusUtils.extractSessionNameFromArn(roleArnWithSession) + if (!sessionName) { + throw new ToolkitError(`Unable to extract session name from ARN: ${roleArnWithSession}`, { + code: SmusErrorCodes.NoUserProfileFound, + }) + } + + // Convert assumed role ARN to IAM role ARN for matching + // Format: arn:aws:sts::ACCOUNT:assumed-role/ROLE_NAME/SESSION_NAME -> arn:aws:iam::ACCOUNT:role/ROLE_NAME + const iamRoleArn = SmusUtils.convertAssumedRoleArnToIamRoleArn(roleArnWithSession) + if (!iamRoleArn) { + throw new ToolkitError(`Unable to convert assumed role ARN to IAM role ARN: ${roleArnWithSession}`, { + code: SmusErrorCodes.NoUserProfileFound, + }) + } + + this.logger.debug( + `DataZoneCustomClientHelper: Extracted session name: ${sessionName}, IAM role ARN: ${iamRoleArn}` + ) + + // Use searchText to filter by role ARN on server side, then filter by session name on client side + let nextToken: string | undefined + let totalProfilesChecked = 0 + + do { + this.logger.debug( + `DataZoneCustomClientHelper: Calling searchUserProfiles with searchText: ${iamRoleArn}` + ) + + const response = await this.searchUserProfiles(domainIdentifier, { + userType: 'DATAZONE_IAM_USER', + searchText: iamRoleArn, // Server-side filter by role ARN + maxResults: 50, + nextToken, + }) + + totalProfilesChecked += response.items.length + this.logger.debug( + `DataZoneCustomClientHelper: Received ${response.items.length} user profiles matching role ARN in current page (total checked: ${totalProfilesChecked})` + ) + + // Find exact match in current page using client-side filtering for session name + // Server-side filtering by role ARN should have already reduced the result set significantly + for (const profile of response.items) { + // Match based on session name (role ARN already filtered by searchText) + // principalId format: PRINCIPAL_ID:SESSION_NAME + const matchesSession = profile.details?.iam?.principalId?.includes(sessionName) + + if (matchesSession) { + this.logger.info( + `DataZoneCustomClientHelper: Found matching user profile with ID: ${profile.id} (role: ${iamRoleArn}, session: ${sessionName}) after checking ${totalProfilesChecked} profiles` + ) + return profile.id! + } + } + + nextToken = response.nextToken + } while (nextToken) + + // No matching profile found after checking all pages + this.logger.error( + `DataZoneCustomClientHelper: No user profile found for role: ${iamRoleArn} with session: ${sessionName} after checking ${totalProfilesChecked} profiles` + ) + throw new ToolkitError(`No user profile found for role: ${iamRoleArn} with session: ${sessionName}`, { + code: SmusErrorCodes.NoUserProfileFound, + }) + } catch (err) { + // Re-throw if it's already a ToolkitError + if (err instanceof ToolkitError) { + throw err + } + + // Log and wrap other errors + this.logger.error('DataZoneCustomClientHelper: Failed to get user profile ID: %s', (err as Error).message) + throw ToolkitError.chain(err, 'Failed to get user profile ID') + } + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/client/datazonecustomclient.json b/packages/core/src/sagemakerunifiedstudio/shared/client/datazonecustomclient.json new file mode 100644 index 00000000000..8414d3340b8 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/datazonecustomclient.json @@ -0,0 +1,919 @@ +{ + "version": "2.0", + "metadata": { + "apiVersion": "2018-05-10", + "auth": ["aws.auth#sigv4"], + "endpointPrefix": "datazone", + "protocol": "rest-json", + "protocols": ["rest-json"], + "serviceFullName": "Amazon DataZone", + "serviceId": "DataZone", + "signatureVersion": "v4", + "signingName": "datazone", + "uid": "datazone-2018-05-10" + }, + "operations": { + "GetDomain": { + "name": "GetDomain", + "http": { + "method": "GET", + "requestUri": "/v2/domains/{identifier}", + "responseCode": 200 + }, + "input": { + "shape": "GetDomainInput" + }, + "output": { + "shape": "GetDomainOutput" + }, + "errors": [ + { + "shape": "InternalServerException" + }, + { + "shape": "ResourceNotFoundException" + }, + { + "shape": "AccessDeniedException" + }, + { + "shape": "ThrottlingException" + }, + { + "shape": "ServiceQuotaExceededException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "UnauthorizedException" + } + ], + "readonly": true + }, + "ListDomains": { + "name": "ListDomains", + "http": { + "method": "GET", + "requestUri": "/v2/domains", + "responseCode": 200 + }, + "input": { + "shape": "ListDomainsInput" + }, + "output": { + "shape": "ListDomainsOutput" + }, + "errors": [ + { + "shape": "InternalServerException" + }, + { + "shape": "ResourceNotFoundException" + }, + { + "shape": "AccessDeniedException" + }, + { + "shape": "ThrottlingException" + }, + { + "shape": "ServiceQuotaExceededException" + }, + { + "shape": "ConflictException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "UnauthorizedException" + } + ], + "readonly": true + }, + "SearchGroupProfiles": { + "name": "SearchGroupProfiles", + "http": { + "method": "POST", + "requestUri": "/v2/domains/{domainIdentifier}/search-group-profiles", + "responseCode": 200 + }, + "input": { + "shape": "SearchGroupProfilesInput" + }, + "output": { + "shape": "SearchGroupProfilesOutput" + }, + "errors": [ + { + "shape": "InternalServerException" + }, + { + "shape": "ResourceNotFoundException" + }, + { + "shape": "AccessDeniedException" + }, + { + "shape": "ThrottlingException" + }, + { + "shape": "ConflictException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "UnauthorizedException" + } + ] + }, + "SearchUserProfiles": { + "name": "SearchUserProfiles", + "http": { + "method": "POST", + "requestUri": "/v2/domains/{domainIdentifier}/search-user-profiles", + "responseCode": 200 + }, + "input": { + "shape": "SearchUserProfilesInput" + }, + "output": { + "shape": "SearchUserProfilesOutput" + }, + "errors": [ + { + "shape": "InternalServerException" + }, + { + "shape": "ResourceNotFoundException" + }, + { + "shape": "AccessDeniedException" + }, + { + "shape": "ThrottlingException" + }, + { + "shape": "ConflictException" + }, + { + "shape": "ValidationException" + }, + { + "shape": "UnauthorizedException" + } + ] + } + }, + "shapes": { + "GetDomainInput": { + "type": "structure", + "required": ["identifier"], + "members": { + "identifier": { + "shape": "DomainId", + "location": "uri", + "locationName": "identifier" + } + } + }, + "DomainId": { + "type": "string", + "pattern": "dzd[-_][a-zA-Z0-9_-]{1,36}" + }, + "GetDomainOutput": { + "type": "structure", + "required": ["id", "status"], + "members": { + "id": { + "shape": "DomainId" + }, + "rootDomainUnitId": { + "shape": "DomainUnitId" + }, + "name": { + "shape": "String" + }, + "description": { + "shape": "String" + }, + "singleSignOn": { + "shape": "SingleSignOn" + }, + "domainExecutionRole": { + "shape": "RoleArn" + }, + "arn": { + "shape": "String" + }, + "kmsKeyIdentifier": { + "shape": "KmsKeyArn" + }, + "status": { + "shape": "DomainStatus" + }, + "failureReasons": { + "shape": "FailureReasonsList" + }, + "portalUrl": { + "shape": "String" + }, + "createdAt": { + "shape": "CreatedAt" + }, + "lastUpdatedAt": { + "shape": "UpdatedAt" + }, + "tags": { + "shape": "Tags" + }, + "provisionStatus": { + "shape": "ProvisionStatus", + "internalonly": true + }, + "domainVersion": { + "shape": "DomainVersion" + }, + "domainServiceRole": { + "shape": "RoleArn", + "deprecated": true, + "internalonly": true + }, + "serviceRole": { + "shape": "RoleArn" + }, + "supportedDomainVersions": { + "shape": "SupportedDomainVersions", + "internalonly": true + }, + "iamSignIns": { + "shape": "IamSignIns", + "internalonly": true + }, + "preferences": { + "shape": "Preferences", + "internalonly": true + } + } + }, + "DomainUnitId": { + "type": "string", + "max": 256, + "min": 1, + "pattern": "[a-z0-9_\\-]+" + }, + "String": { + "type": "string" + }, + "SingleSignOn": { + "type": "structure", + "members": { + "type": { + "shape": "AuthType" + }, + "userAssignment": { + "shape": "UserAssignment" + }, + "idcInstanceArn": { + "shape": "SingleSignOnIdcInstanceArnString" + }, + "ssoUrl": { + "shape": "String", + "internalonly": true + }, + "idcApplicationArn": { + "shape": "String", + "internalonly": true + } + } + }, + "AuthType": { + "type": "string", + "enum": ["IAM_IDC", "DISABLED", "SAML"] + }, + "UserAssignment": { + "type": "string", + "enum": ["AUTOMATIC", "MANUAL"] + }, + "SingleSignOnIdcInstanceArnString": { + "type": "string", + "pattern": ".*arn:(aws|aws-us-gov|aws-cn|aws-iso|aws-iso-b):sso:::instance/(sso)?ins-[a-zA-Z0-9-.]{16}.*" + }, + "RoleArn": { + "type": "string", + "pattern": "arn:aws[^:]*:iam::\\d{12}:role(/[a-zA-Z0-9+=,.@_-]+)*/[a-zA-Z0-9+=,.@_-]+" + }, + "KmsKeyArn": { + "type": "string", + "max": 1024, + "min": 1, + "pattern": "arn:aws(|-cn|-us-gov):kms:[a-zA-Z0-9-]*:[0-9]{12}:key/[a-zA-Z0-9-]{36}" + }, + "DomainStatus": { + "type": "string", + "enum": ["CREATING", "AVAILABLE", "CREATION_FAILED", "DELETING", "DELETED", "DELETION_FAILED"] + }, + "FailureReasonsList": { + "type": "list", + "member": { + "shape": "FailureReason" + } + }, + "FailureReason": { + "type": "structure", + "members": { + "code": { + "shape": "String" + }, + "message": { + "shape": "String" + } + } + }, + "CreatedAt": { + "type": "timestamp" + }, + "UpdatedAt": { + "type": "timestamp" + }, + "Tags": { + "type": "map", + "key": { + "shape": "TagKey" + }, + "value": { + "shape": "TagValue" + } + }, + "TagKey": { + "type": "string", + "max": 128, + "min": 1, + "pattern": "[\\w \\.:/=+@-]+" + }, + "TagValue": { + "type": "string", + "max": 256, + "min": 0, + "pattern": "[\\w \\.:/=+@-]*" + }, + "ProvisionStatus": { + "type": "string", + "enum": [ + "PROVISIONING", + "PROVISIONING_PROJECT_PROFILES", + "PROVISIONING_MODEL_ASSETS", + "PROVISION_FAILED", + "PROVISION_COMPLETE" + ] + }, + "DomainVersion": { + "type": "string", + "enum": ["V1", "V2"] + }, + "SupportedDomainVersions": { + "type": "list", + "member": { + "shape": "DomainVersion" + } + }, + "IamSignIns": { + "type": "list", + "member": { + "shape": "IamSignIn" + }, + "internalonly": true + }, + "IamSignIn": { + "type": "string", + "enum": ["IAM_ROLE", "IAM_USER"], + "internalonly": true + }, + "Preferences": { + "type": "map", + "key": { + "shape": "PreferenceKey" + }, + "value": { + "shape": "PreferenceValue" + }, + "max": 10, + "min": 0 + }, + "PreferenceKey": { + "type": "string", + "max": 128, + "min": 1, + "pattern": "[\\w \\.:/=+@-]+" + }, + "PreferenceValue": { + "type": "string", + "max": 256, + "min": 0, + "pattern": "[\\w \\.:/=+@-]*" + }, + "InternalServerException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { + "shape": "ErrorMessage" + } + }, + "error": { + "httpStatusCode": 500 + }, + "exception": true, + "fault": true, + "retryable": { + "throttling": false + } + }, + "ErrorMessage": { + "type": "string" + }, + "ResourceNotFoundException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { + "shape": "ErrorMessage" + } + }, + "error": { + "httpStatusCode": 404, + "senderFault": true + }, + "exception": true + }, + "AccessDeniedException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { + "shape": "ErrorMessage" + } + }, + "error": { + "httpStatusCode": 403, + "senderFault": true + }, + "exception": true + }, + "ThrottlingException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { + "shape": "ErrorMessage" + } + }, + "error": { + "httpStatusCode": 429, + "senderFault": true + }, + "exception": true, + "retryable": { + "throttling": false + } + }, + "ServiceQuotaExceededException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { + "shape": "ErrorMessage" + } + }, + "error": { + "httpStatusCode": 402, + "senderFault": true + }, + "exception": true + }, + "ValidationException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { + "shape": "ErrorMessage" + } + }, + "error": { + "httpStatusCode": 400, + "senderFault": true + }, + "exception": true + }, + "UnauthorizedException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { + "shape": "ErrorMessage" + } + }, + "error": { + "httpStatusCode": 401, + "senderFault": true + }, + "exception": true + }, + "ListDomainsInput": { + "type": "structure", + "members": { + "status": { + "shape": "DomainStatus", + "location": "querystring", + "locationName": "status" + }, + "maxResults": { + "shape": "MaxResultsForListDomains", + "location": "querystring", + "locationName": "maxResults" + }, + "nextToken": { + "shape": "PaginationToken", + "location": "querystring", + "locationName": "nextToken" + } + } + }, + "MaxResultsForListDomains": { + "type": "integer", + "box": true, + "max": 25, + "min": 1 + }, + "PaginationToken": { + "type": "string", + "max": 8192, + "min": 1 + }, + "ListDomainsOutput": { + "type": "structure", + "required": ["items"], + "members": { + "items": { + "shape": "DomainSummaries" + }, + "domains": { + "shape": "DomainSummaries", + "internalonly": true + }, + "nextToken": { + "shape": "PaginationToken" + } + } + }, + "DomainSummaries": { + "type": "list", + "member": { + "shape": "DomainSummary" + } + }, + "DomainSummary": { + "type": "structure", + "required": ["id", "name", "arn", "managedAccountId", "status", "createdAt"], + "members": { + "id": { + "shape": "DomainId" + }, + "name": { + "shape": "DomainName" + }, + "description": { + "shape": "DomainDescription" + }, + "arn": { + "shape": "String" + }, + "managedAccountId": { + "shape": "String" + }, + "status": { + "shape": "DomainStatus" + }, + "portalUrl": { + "shape": "String" + }, + "createdAt": { + "shape": "CreatedAt" + }, + "lastUpdatedAt": { + "shape": "UpdatedAt" + }, + "domainVersion": { + "shape": "DomainVersion" + }, + "iamSignIns": { + "shape": "IamSignIns", + "internalonly": true + }, + "preferences": { + "shape": "Preferences", + "internalonly": true + } + } + }, + "DomainName": { + "type": "string", + "sensitive": true + }, + "DomainDescription": { + "type": "string", + "sensitive": true + }, + "ConflictException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { + "shape": "ErrorMessage" + }, + "reason": { + "shape": "ConflictReason" + }, + "details": { + "shape": "ConflictDetails" + } + }, + "error": { + "httpStatusCode": 409, + "senderFault": true + }, + "exception": true + }, + "ConflictReason": { + "type": "string", + "enum": ["RESOURCE_LOCKED"] + }, + "ConflictDetails": { + "type": "structure", + "members": { + "lock": { + "shape": "LockDetails" + } + }, + "union": true + }, + "LockDetails": { + "type": "structure", + "required": ["lockedBy", "lockedAt", "lockExpiresAt"], + "members": { + "lockedBy": { + "shape": "String" + }, + "lockedAt": { + "shape": "Timestamp" + }, + "lockExpiresAt": { + "shape": "Timestamp" + } + } + }, + "Timestamp": { + "type": "timestamp" + }, + "SearchGroupProfilesInput": { + "type": "structure", + "required": ["domainIdentifier", "groupType"], + "members": { + "domainIdentifier": { + "shape": "DomainId", + "location": "uri", + "locationName": "domainIdentifier" + }, + "groupType": { + "shape": "GroupSearchType" + }, + "searchText": { + "shape": "GroupSearchText" + }, + "maxResults": { + "shape": "MaxResults" + }, + "nextToken": { + "shape": "PaginationToken" + } + } + }, + "GroupSearchType": { + "type": "string", + "enum": ["SSO_GROUP", "DATAZONE_SSO_GROUP", "IAM_ROLE_SESSION_GROUP"] + }, + "GroupSearchText": { + "type": "string", + "max": 1024, + "min": 0, + "sensitive": true + }, + "MaxResults": { + "type": "integer", + "box": true, + "max": 50, + "min": 1 + }, + "SearchGroupProfilesOutput": { + "type": "structure", + "members": { + "items": { + "shape": "GroupProfileSummaries" + }, + "nextToken": { + "shape": "PaginationToken" + } + } + }, + "GroupProfileSummaries": { + "type": "list", + "member": { + "shape": "GroupProfileSummary" + } + }, + "GroupProfileSummary": { + "type": "structure", + "members": { + "domainId": { + "shape": "DomainId" + }, + "id": { + "shape": "GroupProfileId" + }, + "status": { + "shape": "GroupProfileStatus" + }, + "groupName": { + "shape": "GroupProfileName" + }, + "rolePrincipalArn": { + "shape": "String", + "internalonly": true + }, + "rolePrincipalId": { + "shape": "String", + "internalonly": true + } + } + }, + "GroupProfileId": { + "type": "string", + "pattern": "([0-9a-f]{10}-|)[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}" + }, + "GroupProfileStatus": { + "type": "string", + "enum": ["ASSIGNED", "NOT_ASSIGNED"] + }, + "GroupProfileName": { + "type": "string", + "max": 1024, + "min": 1, + "pattern": "[a-zA-Z_0-9+=,.@-]+", + "sensitive": true + }, + "SearchUserProfilesInput": { + "type": "structure", + "required": ["domainIdentifier", "userType"], + "members": { + "domainIdentifier": { + "shape": "DomainId", + "location": "uri", + "locationName": "domainIdentifier" + }, + "userType": { + "shape": "UserSearchType" + }, + "searchText": { + "shape": "UserSearchText" + }, + "maxResults": { + "shape": "MaxResults" + }, + "nextToken": { + "shape": "PaginationToken" + } + } + }, + "UserSearchType": { + "type": "string", + "enum": ["SSO_USER", "DATAZONE_USER", "DATAZONE_SSO_USER", "DATAZONE_IAM_USER"] + }, + "UserSearchText": { + "type": "string", + "max": 1024, + "min": 0, + "sensitive": true + }, + "SearchUserProfilesOutput": { + "type": "structure", + "members": { + "items": { + "shape": "UserProfileSummaries" + }, + "nextToken": { + "shape": "PaginationToken" + } + } + }, + "UserProfileSummaries": { + "type": "list", + "member": { + "shape": "UserProfileSummary" + } + }, + "UserProfileSummary": { + "type": "structure", + "members": { + "domainId": { + "shape": "DomainId" + }, + "id": { + "shape": "UserProfileId" + }, + "type": { + "shape": "UserProfileType" + }, + "status": { + "shape": "UserProfileStatus" + }, + "details": { + "shape": "UserProfileDetails" + } + } + }, + "UserProfileId": { + "type": "string", + "pattern": "([0-9a-f]{10}-|)[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}" + }, + "UserProfileType": { + "type": "string", + "enum": ["IAM", "SSO", "SAML"] + }, + "UserProfileStatus": { + "type": "string", + "enum": ["ASSIGNED", "NOT_ASSIGNED", "ACTIVATED", "DEACTIVATED", "ARCHIVED"] + }, + "UserProfileDetails": { + "type": "structure", + "members": { + "iam": { + "shape": "IamUserProfileDetails" + }, + "sso": { + "shape": "SsoUserProfileDetails" + } + }, + "union": true + }, + "IamUserProfileDetails": { + "type": "structure", + "members": { + "arn": { + "shape": "String" + }, + "principalId": { + "shape": "String" + } + } + }, + "SsoUserProfileDetails": { + "type": "structure", + "members": { + "username": { + "shape": "UserProfileName" + }, + "firstName": { + "shape": "FirstName" + }, + "lastName": { + "shape": "LastName" + }, + "email": { + "shape": "String", + "internalonly": true + }, + "userId": { + "shape": "String", + "internalonly": true + } + } + }, + "UserProfileName": { + "type": "string", + "max": 1024, + "min": 1, + "pattern": "[a-zA-Z_0-9+=,.@-]+", + "sensitive": true + }, + "FirstName": { + "type": "string", + "sensitive": true + }, + "LastName": { + "type": "string", + "sensitive": true + } + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/client/glueCatalogClient.ts b/packages/core/src/sagemakerunifiedstudio/shared/client/glueCatalogClient.ts new file mode 100644 index 00000000000..1cc3c29e294 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/glueCatalogClient.ts @@ -0,0 +1,122 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLogger } from '../../../shared/logger/logger' +import { GlueCatalog, Catalog } from '@amzn/glue-catalog-client' +import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider' + +/** + * Client for interacting with Glue Catalog API + */ +export class GlueCatalogClient { + private glueClient: GlueCatalog | undefined + private static instance: GlueCatalogClient | undefined + private readonly logger = getLogger('smus') + + private constructor( + private readonly region: string, + private readonly connectionCredentialsProvider?: ConnectionCredentialsProvider + ) {} + + /** + * Gets a singleton instance of the GlueCatalogClient + * @returns GlueCatalogClient instance + */ + public static getInstance(region: string): GlueCatalogClient { + if (!GlueCatalogClient.instance) { + GlueCatalogClient.instance = new GlueCatalogClient(region) + } + return GlueCatalogClient.instance + } + + /** + * Creates a new GlueCatalogClient instance with specific credentials + * @param region AWS region + * @param credentials AWS credentials + * @returns GlueCatalogClient instance with credentials + */ + public static createWithCredentials( + region: string, + connectionCredentialsProvider: ConnectionCredentialsProvider + ): GlueCatalogClient { + return new GlueCatalogClient(region, connectionCredentialsProvider) + } + + /** + * Gets the AWS region + * @returns AWS region + */ + public getRegion(): string { + return this.region + } + + /** + * Lists Glue catalogs with pagination support + * @param nextToken Optional pagination token + * @returns Object containing catalogs and nextToken + */ + public async getCatalogs(nextToken?: string): Promise<{ catalogs: Catalog[]; nextToken?: string }> { + try { + this.logger.info(`GlueCatalogClient: Getting catalogs in region ${this.region}`) + + const glueClient = await this.getGlueCatalogClient() + + // Call the GetCatalogs API with pagination + const response = await glueClient.getCatalogs({ + Recursive: true, + NextToken: nextToken, + }) + + const catalogs: Catalog[] = response.CatalogList || [] + + this.logger.info(`GlueCatalogClient: Found ${catalogs.length} catalogs in this page`) + return { + catalogs, + nextToken: response.NextToken, + } + } catch (err) { + this.logger.error('GlueCatalogClient: Failed to get catalogs: %s', err as Error) + throw err + } + } + + /** + * Gets the Glue client, initializing it if necessary + */ + private async getGlueCatalogClient(): Promise { + if (!this.glueClient) { + try { + if (this.connectionCredentialsProvider) { + // Create client with credential provider function for auto-refresh + const awsCredentialProvider = async () => { + const credentials = await this.connectionCredentialsProvider!.getCredentials() + return { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.sessionToken, + expiration: credentials.expiration, + } + } + + this.glueClient = new GlueCatalog({ + region: this.region, + credentials: awsCredentialProvider, + }) + } else { + // Use default credentials + this.glueClient = new GlueCatalog({ + region: this.region, + }) + } + + this.logger.debug('GlueCatalogClient: Successfully created Glue client') + } catch (err) { + this.logger.error('GlueCatalogClient: Failed to create Glue client: %s', err as Error) + throw err + } + } + return this.glueClient + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/client/glueClient.ts b/packages/core/src/sagemakerunifiedstudio/shared/client/glueClient.ts new file mode 100644 index 00000000000..cad84d23332 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/glueClient.ts @@ -0,0 +1,166 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + Glue, + GetDatabasesCommand, + GetTablesCommand, + GetTableCommand, + Table, + ResourceShareType, + DatabaseAttributes, + TableAttributes, + Database, +} from '@aws-sdk/client-glue' +import { getLogger } from '../../../shared/logger/logger' +import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider' + +/** + * Client for interacting with AWS Glue API using public SDK + */ +export class GlueClient { + private glueClient: Glue | undefined + private readonly logger = getLogger('smus') + + constructor( + private readonly region: string, + private readonly connectionCredentialsProvider: ConnectionCredentialsProvider + ) {} + + /** + * Gets databases from a catalog + * @param catalogId Optional catalog ID (uses default if not provided) + * @param nextToken Optional pagination token + * @returns List of databases + */ + public async getDatabases( + catalogId?: string, + resourceShareType?: ResourceShareType, + attributesToGet?: DatabaseAttributes[], + nextToken?: string + ): Promise<{ databases: Database[]; nextToken?: string }> { + try { + this.logger.info(`GlueClient: Getting databases for catalog ${catalogId || 'default'}`) + + const glueClient = await this.getGlueClient() + const response = await glueClient.send( + new GetDatabasesCommand({ + CatalogId: catalogId, + ResourceShareType: resourceShareType, + AttributesToGet: attributesToGet, + NextToken: nextToken, + MaxResults: 100, + }) + ) + + const databases = response.DatabaseList || [] + this.logger.info(`GlueClient: Found ${databases.length} databases`) + + return { + databases, + nextToken: response.NextToken, + } + } catch (err) { + this.logger.error('GlueClient: Failed to get databases: %s', err as Error) + throw err + } + } + + /** + * Gets tables from a database + * @param databaseName Database name + * @param catalogId Optional catalog ID + * @param nextToken Optional pagination token + * @returns List of tables + */ + public async getTables( + databaseName: string, + catalogId?: string, + attributesToGet?: TableAttributes[], + nextToken?: string + ): Promise<{ tables: Table[]; nextToken?: string }> { + try { + this.logger.info(`GlueClient: Getting tables for database ${databaseName}`) + + const glueClient = await this.getGlueClient() + const response = await glueClient.send( + new GetTablesCommand({ + DatabaseName: databaseName, + CatalogId: catalogId, + AttributesToGet: attributesToGet, + NextToken: nextToken, + MaxResults: 100, + }) + ) + + const tables = response.TableList || [] + this.logger.info(`GlueClient: Found ${tables.length} tables`) + + return { + tables, + nextToken: response.NextToken, + } + } catch (err) { + this.logger.error('GlueClient: Failed to get tables: %s', err as Error) + throw err + } + } + + /** + * Gets table details including columns + * @param databaseName Database name + * @param tableName Table name + * @param catalogId Optional catalog ID + * @returns Table details with columns + */ + public async getTable(databaseName: string, tableName: string, catalogId?: string): Promise { + try { + this.logger.info(`GlueClient: Getting table ${tableName} from database ${databaseName}`) + + const glueClient = await this.getGlueClient() + const response = await glueClient.send( + new GetTableCommand({ + DatabaseName: databaseName, + Name: tableName, + CatalogId: catalogId, + }) + ) + + return response.Table + } catch (err) { + this.logger.error('GlueClient: Failed to get table: %s', err as Error) + throw err + } + } + + /** + * Gets the Glue client, initializing it if necessary + */ + private async getGlueClient(): Promise { + if (!this.glueClient) { + try { + const credentialsProvider = async () => { + const credentials = await this.connectionCredentialsProvider.getCredentials() + return { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.sessionToken, + expiration: credentials.expiration, + } + } + + this.glueClient = new Glue({ + region: this.region, + credentials: credentialsProvider, + }) + this.logger.debug('GlueClient: Successfully created Glue client') + } catch (err) { + this.logger.error('GlueClient: Failed to create Glue client: %s', err as Error) + throw err + } + } + return this.glueClient + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/client/s3Client.ts b/packages/core/src/sagemakerunifiedstudio/shared/client/s3Client.ts new file mode 100644 index 00000000000..5375c3d5e92 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/s3Client.ts @@ -0,0 +1,181 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { S3, ListBucketsCommand } from '@aws-sdk/client-s3' +import { getLogger } from '../../../shared/logger/logger' +import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider' + +/** + * Represents an S3 path (bucket or prefix) + */ +export interface S3Path { + bucket: string + prefix?: string + displayName: string + isFolder: boolean + size?: number + lastModified?: Date +} + +/** + * Client for interacting with AWS S3 API using project credentials + */ +export class S3Client { + private s3Client: S3 | undefined + private readonly logger = getLogger('smus') + + constructor( + private readonly region: string, + private readonly connectionCredentialsProvider: ConnectionCredentialsProvider + ) {} + + /** + * Lists S3 paths (folders and objects) using prefix-based navigation + * Uses S3's hierarchical folder-like structure by leveraging prefixes and delimiters + * @param bucket S3 bucket name to list objects from + * @param prefix Optional prefix to filter objects (acts like a folder path) + * @param continuationToken Optional continuation token for pagination + * @returns Object containing paths and nextToken for pagination + */ + public async listPaths( + bucket: string, + prefix?: string, + continuationToken?: string + ): Promise<{ paths: S3Path[]; nextToken?: string }> { + try { + this.logger.info(`S3Client: Listing paths in bucket ${bucket} with prefix ${prefix}`) + + const s3Client = await this.getS3Client() + + // Call S3 ListObjectsV2 API with delimiter to simulate folder structure + // Delimiter '/' treats forward slashes as folder separators + // This returns both CommonPrefixes (folders) and Contents (files) + const response = await s3Client.listObjectsV2({ + Bucket: bucket, + Prefix: prefix, // Filter objects that start with this prefix + Delimiter: '/', // Treat '/' as folder separator for hierarchical listing + ContinuationToken: continuationToken, // For pagination + }) + + const paths: S3Path[] = [] + + // Process CommonPrefixes - these represent "folders" in S3 + // CommonPrefixes are object keys that share a common prefix up to the delimiter + if (response.CommonPrefixes) { + for (const commonPrefix of response.CommonPrefixes) { + if (commonPrefix.Prefix) { + // Extract folder name by removing the parent prefix and trailing slash + // Example: if prefix="folder1/" and commonPrefix="folder1/subfolder/" + // folderName becomes "subfolder" + const folderName = commonPrefix.Prefix.replace(prefix || '', '').replace('/', '') + paths.push({ + bucket, + prefix: commonPrefix.Prefix, // Full S3 prefix for this folder + displayName: folderName, // Human-readable folder name + isFolder: true, // Mark as folder for UI rendering + }) + } + } + } + + // Process Contents - these represent actual S3 objects (files) + if (response.Contents) { + for (const object of response.Contents) { + // Skip if no key or if key matches the prefix exactly (folder itself) + if (object.Key && object.Key !== prefix) { + // Extract file name by removing the parent prefix + // Example: if prefix="folder1/" and object.Key="folder1/file.txt" + // fileName becomes "file.txt" + const fileName = object.Key.replace(prefix || '', '') + + // Only include actual files (not folder markers ending with '/') + if (fileName && !fileName.endsWith('/')) { + paths.push({ + bucket, + prefix: object.Key, // Full S3 object key + displayName: fileName, // Human-readable file name + isFolder: false, // Mark as file for UI rendering + size: object.Size, // File size in bytes + lastModified: object.LastModified, // Last modification timestamp + }) + } + } + } + } + + this.logger.info(`S3Client: Found ${paths.length} paths in bucket ${bucket}`) + return { + paths, + nextToken: response.NextContinuationToken, + } + } catch (err) { + this.logger.error('S3Client: Failed to list paths: %s', err as Error) + throw err + } + } + + /** + * Lists all S3 buckets accessible to the current credentials + * @returns Array of bucket objects + */ + public async listBuckets(): Promise> { + try { + this.logger.debug('S3Client: Listing all accessible buckets') + + const s3Client = await this.getS3Client() + const allBuckets: Array<{ Name?: string; CreationDate?: Date }> = [] + let continuationToken: string | undefined + + do { + const response = await s3Client.send( + new ListBucketsCommand({ + ContinuationToken: continuationToken, + BucketRegion: this.region, + }) + ) + + if (response.Buckets) { + allBuckets.push(...response.Buckets) + } + continuationToken = response.ContinuationToken + } while (continuationToken) + + this.logger.debug(`S3Client: Found ${allBuckets.length} accessible buckets`) + return allBuckets + } catch (err) { + this.logger.error('S3Client: Failed to list buckets: %s', err as Error) + throw err + } + } + + /** + * Gets the S3 client, initializing it if necessary + */ + private async getS3Client(): Promise { + if (!this.s3Client) { + try { + const credentialsProvider = async () => { + const credentials = await this.connectionCredentialsProvider.getCredentials() + return { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.sessionToken, + expiration: credentials.expiration, + } + } + + this.s3Client = new S3({ + region: this.region, + credentials: credentialsProvider, + }) + this.logger.debug('S3Client: Successfully created S3 client') + } catch (err) { + this.logger.error('S3Client: Failed to create S3 client: %s', err as Error) + throw err + } + } + return this.s3Client + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/client/sqlWorkbenchClient.ts b/packages/core/src/sagemakerunifiedstudio/shared/client/sqlWorkbenchClient.ts new file mode 100644 index 00000000000..76527d1d622 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/sqlWorkbenchClient.ts @@ -0,0 +1,318 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Service } from 'aws-sdk' +import { ServiceConfigurationOptions } from 'aws-sdk/lib/service' +import globals from '../../../shared/extensionGlobals' +import { getLogger } from '../../../shared/logger/logger' +import * as SQLWorkbench from './sqlworkbench' +import apiConfig = require('./sqlworkbench.json') +import { v4 as uuidv4 } from 'uuid' +import { getRedshiftTypeFromHost } from '../../explorer/nodes/utils' +import { DatabaseIntegrationConnectionAuthenticationTypes, RedshiftType } from '../../explorer/nodes/types' +import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider' +import { adaptConnectionCredentialsProvider } from './credentialsAdapter' + +/** + * Connection configuration for SQL Workbench + */ +export interface ConnectionConfig { + id: string + type: string + databaseType: string + connectableResourceIdentifier: string + connectableResourceType: string + database: string + auth?: { + secretArn?: string + } +} + +/** + * Resource parent information + */ +export interface ParentResource { + parentId: string + parentType: string +} + +/** + * Gets a SQL Workbench ARN + * @param region AWS region + * @param accountId Optional AWS account ID (will be determined if not provided) + * @returns SQL Workbench ARN + */ +export async function generateSqlWorkbenchArn(region: string, accountId: string): Promise { + return `arn:aws:sqlworkbench:${region}:${accountId}:connection/${uuidv4()}` +} + +/** + * Creates a connection configuration for Redshift + */ +export async function createRedshiftConnectionConfig( + host: string, + database: string, + accountId: string, + region: string, + secretArn?: string, + isGlueCatalogDatabase?: boolean +): Promise { + // Get Redshift deployment type from host + const redshiftDeploymentType = getRedshiftTypeFromHost(host) + + // Extract resource identifier from host + const resourceIdentifier = host.split('.')[0] + + if (!resourceIdentifier) { + throw new Error('Resource identifier could not be determined from host') + } + + // Create connection ID using the proper ARN format + const connectionId = await generateSqlWorkbenchArn(region, accountId) + + // Determine if serverless or cluster based on deployment type + const isServerless = + redshiftDeploymentType === RedshiftType.Serverless || + redshiftDeploymentType === RedshiftType.ServerlessDev || + redshiftDeploymentType === RedshiftType.ServerlessQA + + const isCluster = + redshiftDeploymentType === RedshiftType.Cluster || + redshiftDeploymentType === RedshiftType.ClusterDev || + redshiftDeploymentType === RedshiftType.ClusterQA + + // Validate the Redshift type + if (!isServerless && !isCluster) { + throw new Error(`Unsupported Redshift type for host: ${host}`) + } + + // Determine auth type based on the provided parameters + let authType: string + + if (secretArn) { + authType = DatabaseIntegrationConnectionAuthenticationTypes.SECRET + } else if (isCluster) { + authType = DatabaseIntegrationConnectionAuthenticationTypes.TEMPORARY_CREDENTIALS_WITH_IAM + } else { + // For serverless + authType = DatabaseIntegrationConnectionAuthenticationTypes.FEDERATED + } + + // Enforce specific authentication type for S3Table/RedLake databases + if (isGlueCatalogDatabase) { + authType = isServerless + ? DatabaseIntegrationConnectionAuthenticationTypes.FEDERATED + : DatabaseIntegrationConnectionAuthenticationTypes.TEMPORARY_CREDENTIALS_WITH_IAM + } + + // Create the connection configuration + const connectionConfig: ConnectionConfig = { + id: connectionId, + type: authType, + databaseType: 'REDSHIFT', + connectableResourceIdentifier: resourceIdentifier, + connectableResourceType: isServerless ? 'WORKGROUP' : 'CLUSTER', + database: database, + } + + // Add auth object for SECRET authentication type + if ( + (authType as DatabaseIntegrationConnectionAuthenticationTypes) === + DatabaseIntegrationConnectionAuthenticationTypes.SECRET && + secretArn + ) { + connectionConfig.auth = { secretArn } + } + + return connectionConfig +} + +/** + * Client for interacting with SQL Workbench API + */ +export class SQLWorkbenchClient { + private sqlClient: SQLWorkbench | undefined + private static instance: SQLWorkbenchClient | undefined + private readonly logger = getLogger('smus') + + private constructor( + private readonly region: string, + private readonly connectionCredentialsProvider?: ConnectionCredentialsProvider + ) {} + + /** + * Gets a singleton instance of the SQLWorkbenchClient + * @returns SQLWorkbenchClient instance + */ + public static getInstance(region: string): SQLWorkbenchClient { + if (!SQLWorkbenchClient.instance) { + SQLWorkbenchClient.instance = new SQLWorkbenchClient(region) + } + return SQLWorkbenchClient.instance + } + + /** + * Creates a new SQLWorkbenchClient instance with specific credentials + * @param region AWS region + * @param connectionCredentialsProvider ConnectionCredentialsProvider + * @returns SQLWorkbenchClient instance with credentials provider + */ + public static createWithCredentials( + region: string, + connectionCredentialsProvider: ConnectionCredentialsProvider + ): SQLWorkbenchClient { + return new SQLWorkbenchClient(region, connectionCredentialsProvider) + } + + /** + * Gets the AWS region + * @returns AWS region + */ + public getRegion(): string { + return this.region + } + + /** + * Gets resources from SQL Workbench + * @param params Request parameters + * @returns Raw response from getResources API + */ + public async getResources(params: { + connection: ConnectionConfig + resourceType: string + includeChildren?: boolean + maxItems?: number + parents?: ParentResource[] + pageToken?: string + forceRefresh?: boolean + }): Promise { + try { + this.logger.info(`SQLWorkbenchClient: Getting resources in region ${this.region}`) + + const sqlClient = await this.getSQLClient() + + const requestParams = { + connection: params.connection, + type: params.resourceType, + maxItems: params.maxItems || 100, + parents: params.parents || [], + pageToken: params.pageToken, + forceRefresh: params.forceRefresh || true, + accountSettings: {}, + } + + // Call the GetResources API + const response = await sqlClient.getResources(requestParams).promise() + + return { + resources: response.resources || [], + nextToken: response.nextToken, + } + } catch (err) { + this.logger.error('SQLWorkbenchClient: Failed to get resources: %s', err as Error) + throw err + } + } + + /** + * Execute a SQL query + * @param connectionConfig Connection configuration + * @param query SQL query to execute + * @returns Query execution ID + */ + public async executeQuery(connectionConfig: ConnectionConfig, query: string): Promise { + try { + this.logger.info(`SQLWorkbenchClient: Executing query in region ${this.region}`) + + const sqlClient = await this.getSQLClient() + + // Call the ExecuteQuery API + const response = await sqlClient + .executeQuery({ + connection: connectionConfig as any, + databaseType: 'REDSHIFT', + accountSettings: {}, + executionContext: [ + { + parentType: 'DATABASE', + parentId: connectionConfig.database || '', + }, + ], + query, + queryExecutionType: 'NO_SESSION', + queryResponseDeliveryType: 'ASYNC', + maxItems: 100, + ignoreHistory: true, + tabId: 'data_explorer', + }) + .promise() + + // Log the response + this.logger.info( + `SQLWorkbenchClient: Query execution started with ID: ${response.queryExecutions?.[0]?.queryExecutionId}` + ) + + return response.queryExecutions?.[0]?.queryExecutionId + } catch (err) { + this.logger.error('SQLWorkbenchClient: Failed to execute query: %s', err as Error) + throw err + } + } + + /** + * Gets the SQL client, initializing it if necessary + */ + /** + * Gets the SQL Workbench endpoint URL for the given region + * @param region AWS region + * @returns SQL Workbench endpoint URL + */ + private getSQLWorkbenchEndpoint(region: string): string { + return `https://api-v2.sqlworkbench.${region}.amazonaws.com` + } + + private async getSQLClient(): Promise { + if (!this.sqlClient) { + try { + // Get the endpoint URL for the region + const endpoint = this.getSQLWorkbenchEndpoint(this.region) + this.logger.info(`Using SQL Workbench endpoint: ${endpoint}`) + + if (this.connectionCredentialsProvider) { + // Create client with provided credentials + this.sqlClient = (await globals.sdkClientBuilder.createAwsService( + Service, + { + apiConfig: apiConfig, + region: this.region, + endpoint: endpoint, + credentialProvider: adaptConnectionCredentialsProvider(this.connectionCredentialsProvider), + } as ServiceConfigurationOptions, + undefined, + false + )) as SQLWorkbench + } else { + // Use the SDK client builder for default credentials + this.sqlClient = (await globals.sdkClientBuilder.createAwsService( + Service, + { + apiConfig: apiConfig, + region: this.region, + endpoint: endpoint, + } as ServiceConfigurationOptions, + undefined, + false + )) as SQLWorkbench + } + + this.logger.debug('SQLWorkbenchClient: Successfully created SQL client') + } catch (err) { + this.logger.error('SQLWorkbenchClient: Failed to create SQL client: %s', err as Error) + throw err + } + } + return this.sqlClient + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/client/sqlworkbench.json b/packages/core/src/sagemakerunifiedstudio/shared/client/sqlworkbench.json new file mode 100644 index 00000000000..e403ec34a88 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/sqlworkbench.json @@ -0,0 +1,2102 @@ +{ + "version": "2.0", + "metadata": { + "apiVersion": "2024-02-12", + "auth": ["aws.auth#sigv4"], + "endpointPrefix": "sqlworkbench", + "protocol": "rest-json", + "protocols": ["rest-json"], + "serviceFullName": "AmazonSQLWorkbench", + "serviceId": "SQLWorkbench", + "signatureVersion": "v4", + "signingName": "sqlworkbench", + "uid": "sqlworkbench-2024-02-12" + }, + "operations": { + "CancelQueries": { + "name": "CancelQueries", + "http": { + "method": "POST", + "requestUri": "/database/cancelQueries", + "responseCode": 200 + }, + "input": { "shape": "CancelQueriesRequest" }, + "output": { "shape": "CancelQueriesResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "BadRequestError" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "CreateConnection": { + "name": "CreateConnection", + "http": { + "method": "PUT", + "requestUri": "/connections", + "responseCode": 200 + }, + "input": { "shape": "CreateConnectionRequest" }, + "output": { "shape": "CreateConnectionResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "DeleteConnection": { + "name": "DeleteConnection", + "http": { + "method": "DELETE", + "requestUri": "/connections/{connectionId}", + "responseCode": 200 + }, + "input": { "shape": "DeleteConnectionRequest" }, + "output": { "shape": "DeleteConnectionResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "ExecuteQuery": { + "name": "ExecuteQuery", + "http": { + "method": "POST", + "requestUri": "/database/executeQuery", + "responseCode": 200 + }, + "input": { "shape": "ExecuteQueryRequest" }, + "output": { "shape": "ExecuteQueryResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "BadRequestError" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "ExportQueryResults": { + "name": "ExportQueryResults", + "http": { + "method": "POST", + "requestUri": "/database/exportResults", + "responseCode": 200 + }, + "input": { "shape": "ExportQueryResultsRequest" }, + "output": { "shape": "ExportQueryResultsResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "BadRequestError" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "GetConnectableResources": { + "name": "GetConnectableResources", + "http": { + "method": "POST", + "requestUri": "/database/getConnectableResources", + "responseCode": 200 + }, + "input": { "shape": "GetConnectableResourcesRequest" }, + "output": { "shape": "GetConnectableResourcesResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "BadRequestError" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "GetConnection": { + "name": "GetConnection", + "http": { + "method": "GET", + "requestUri": "/connections/{connectionId}", + "responseCode": 200 + }, + "input": { "shape": "GetConnectionRequest" }, + "output": { "shape": "GetConnectionResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "GetDatabaseConfigurations": { + "name": "GetDatabaseConfigurations", + "http": { + "method": "POST", + "requestUri": "/database/configurations", + "responseCode": 200 + }, + "input": { "shape": "GetDatabaseConfigurationsRequest" }, + "output": { "shape": "GetDatabaseConfigurationsResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "BadRequestError" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "GetQueryExecutionHistory": { + "name": "GetQueryExecutionHistory", + "http": { + "method": "POST", + "requestUri": "/queryExecutionHistory/details", + "responseCode": 200 + }, + "input": { "shape": "GetQueryExecutionHistoryRequest" }, + "output": { "shape": "GetQueryExecutionHistoryResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "BadRequestError" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "GetQueryResult": { + "name": "GetQueryResult", + "http": { + "method": "POST", + "requestUri": "/database/getQueryResults", + "responseCode": 200 + }, + "input": { "shape": "GetQueryResultRequest" }, + "output": { "shape": "GetQueryResultResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "BadRequestError" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "GetResources": { + "name": "GetResources", + "http": { + "method": "POST", + "requestUri": "/database/getResources", + "responseCode": 200 + }, + "input": { "shape": "GetResourcesRequest" }, + "output": { "shape": "GetResourcesResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "BadRequestError" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "GetTabStates": { + "name": "GetTabStates", + "http": { + "method": "POST", + "requestUri": "/tab/state", + "responseCode": 200 + }, + "input": { "shape": "GetTabStatesRequest" }, + "output": { "shape": "GetTabStatesResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "BadRequestError" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "ListQueryExecutionHistory": { + "name": "ListQueryExecutionHistory", + "http": { + "method": "POST", + "requestUri": "/queryExecutionHistory/list", + "responseCode": 200 + }, + "input": { "shape": "ListQueryExecutionHistoryRequest" }, + "output": { "shape": "ListQueryExecutionHistoryResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "BadRequestError" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "ListTagsForResource": { + "name": "ListTagsForResource", + "http": { + "method": "GET", + "requestUri": "/tags/{resourceArn}", + "responseCode": 200 + }, + "input": { "shape": "ListTagsForResourceRequest" }, + "output": { "shape": "ListTagsForResourceResponse" }, + "errors": [ + { "shape": "BadRequestError" }, + { "shape": "ThrottlingException" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "PollQueryExecutionEvents": { + "name": "PollQueryExecutionEvents", + "http": { + "method": "POST", + "requestUri": "/database/pollQueryExecutionEvents", + "responseCode": 200 + }, + "input": { "shape": "PollQueryExecutionEventsRequest" }, + "output": { "shape": "PollQueryExecutionEventsResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "BadRequestError" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "TagResource": { + "name": "TagResource", + "http": { + "method": "POST", + "requestUri": "/tags/{resourceArn}", + "responseCode": 204 + }, + "input": { "shape": "TagResourceRequest" }, + "output": { "shape": "TagResourceResponse" }, + "errors": [ + { "shape": "BadRequestError" }, + { "shape": "ThrottlingException" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "UntagResource": { + "name": "UntagResource", + "http": { + "method": "DELETE", + "requestUri": "/tags/{resourceArn}", + "responseCode": 204 + }, + "input": { "shape": "UntagResourceRequest" }, + "output": { "shape": "UntagResourceResponse" }, + "errors": [ + { "shape": "BadRequestError" }, + { "shape": "ThrottlingException" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ], + "idempotent": true + }, + "UpdateConnection": { + "name": "UpdateConnection", + "http": { + "method": "POST", + "requestUri": "/connections", + "responseCode": 200 + }, + "input": { "shape": "UpdateConnectionRequest" }, + "output": { "shape": "UpdateConnectionResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerError" }, + { "shape": "ValidationException" } + ] + }, + "VerifyResourcesExistForTagris": { + "name": "VerifyResourcesExistForTagris", + "http": { + "method": "POST", + "requestUri": "/verifyResourcesExistForTagris", + "responseCode": 200 + }, + "input": { "shape": "TagrisVerifyResourcesExistInput" }, + "output": { "shape": "TagrisVerifyResourcesExistOutput" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "InternalServerError" }, + { "shape": "TagrisInvalidParameterException" }, + { "shape": "TagrisAccessDeniedException" }, + { "shape": "TagrisInvalidArnException" }, + { "shape": "ResourceNotFoundException" }, + { "shape": "TagrisInternalServiceException" }, + { "shape": "ServiceQuotaExceededException" }, + { "shape": "AccessDeniedException" }, + { "shape": "TagrisPartialResourcesExistResultsException" }, + { "shape": "TagrisThrottledException" }, + { "shape": "ConflictException" }, + { "shape": "ValidationException" } + ] + } + }, + "shapes": { + "AccessDeniedException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { "shape": "String" }, + "code": { "shape": "ErrorCode" } + }, + "error": { + "httpStatusCode": 403, + "senderFault": true + }, + "exception": true + }, + "AckIds": { + "type": "list", + "member": { "shape": "AckIdsMemberString" } + }, + "AckIdsMemberString": { + "type": "string", + "max": 100, + "min": 0 + }, + "Arn": { + "type": "string", + "max": 1011, + "min": 20 + }, + "AvailableConnectionConfigurationOptions": { + "type": "list", + "member": { "shape": "AvailableConnectionConfigurationOptionsMemberString" } + }, + "AvailableConnectionConfigurationOptionsMemberString": { + "type": "string", + "max": 50, + "min": 0 + }, + "BadRequestError": { + "type": "structure", + "required": ["message"], + "members": { + "message": { "shape": "String" }, + "code": { "shape": "ErrorCode" } + }, + "error": { + "httpStatusCode": 400, + "senderFault": true + }, + "exception": true + }, + "Boolean": { + "type": "boolean", + "box": true + }, + "CancelQueriesRequest": { + "type": "structure", + "required": ["queryExecutionIds", "databaseType"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "accountSettings": { "shape": "DatabaseConnectionAccountSettings" }, + "queryExecutionIds": { "shape": "CancelQueriesRequestQueryExecutionIdsList" }, + "databaseType": { + "shape": "DatabaseType", + "location": "querystring", + "locationName": "databaseType" + } + } + }, + "CancelQueriesRequestQueryExecutionIdsList": { + "type": "list", + "member": { "shape": "CancelQueriesRequestQueryExecutionIdsListMemberString" }, + "max": 100, + "min": 1 + }, + "CancelQueriesRequestQueryExecutionIdsListMemberString": { + "type": "string", + "max": 100, + "min": 1 + }, + "CancelQueriesResponse": { + "type": "structure", + "required": ["cancelQueryResponses"], + "members": { + "cancelQueryResponses": { "shape": "CancelQueryResponses" } + } + }, + "CancelQueryResponse": { + "type": "structure", + "required": ["queryExecutionId"], + "members": { + "queryExecutionId": { "shape": "CancelQueryResponseQueryExecutionIdString" }, + "queryCancellationStatus": { "shape": "QueryCancellationStatus" } + } + }, + "CancelQueryResponseQueryExecutionIdString": { + "type": "string", + "max": 1000, + "min": 0 + }, + "CancelQueryResponses": { + "type": "list", + "member": { "shape": "CancelQueryResponse" } + }, + "ChildObjectTypes": { + "type": "list", + "member": { "shape": "ChildObjectTypesMemberString" } + }, + "ChildObjectTypesMemberString": { + "type": "string", + "max": 50, + "min": 0 + }, + "Columns": { + "type": "list", + "member": { "shape": "QueryResultCellValue" } + }, + "ConflictException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { "shape": "String" }, + "code": { "shape": "ErrorCode" } + }, + "error": { + "httpStatusCode": 409, + "senderFault": true + }, + "exception": true + }, + "ConnectableResource": { + "type": "structure", + "required": ["displayName", "identifier", "childObjectTypes", "availableConnectionConfigurationOptions"], + "members": { + "displayName": { "shape": "ResourceDisplayName" }, + "identifier": { "shape": "ResourceIdentifier" }, + "type": { "shape": "ConnectableResourceTypeString" }, + "unavailable": { "shape": "Boolean" }, + "tooltipTranslationKey": { "shape": "ConnectableResourceTooltipTranslationKeyString" }, + "childObjectTypes": { "shape": "ChildObjectTypes" }, + "availableConnectionConfigurationOptions": { "shape": "AvailableConnectionConfigurationOptions" } + } + }, + "ConnectableResourceTooltipTranslationKeyString": { + "type": "string", + "max": 50, + "min": 0 + }, + "ConnectableResourceTypeString": { + "type": "string", + "max": 50, + "min": 0 + }, + "ConnectableResourceTypes": { + "type": "list", + "member": { "shape": "ConnectableResourceTypesMemberString" } + }, + "ConnectableResourceTypesMemberString": { + "type": "string", + "max": 50, + "min": 0 + }, + "ConnectableResources": { + "type": "list", + "member": { "shape": "ConnectableResource" } + }, + "Connection": { + "type": "structure", + "members": { + "id": { + "shape": "String", + "documentation": "

Id of the connection

" + }, + "name": { + "shape": "ConnectionName", + "documentation": "

Name of the connection

" + }, + "authenticationType": { + "shape": "ConnectionAuthenticationTypes", + "documentation": "

Number representing the type of authentication to use (2 = IAM, 3 = Username and Password). Today we only support the types 2 and 3

" + }, + "secretArn": { + "shape": "String", + "documentation": "

Secret that is linked to this connection

" + }, + "databaseName": { + "shape": "DatabaseName", + "documentation": "

Name of the database where the query is run

" + }, + "clusterId": { + "shape": "String", + "documentation": "

Id of the cluster of the connection

" + }, + "dbUser": { + "shape": "DbUser", + "documentation": "

User of the database

" + }, + "isServerless": { "shape": "Boolean" }, + "isProd": { "shape": "String" }, + "isEnabled": { "shape": "String" }, + "userSettings": { "shape": "UserSettings" }, + "recordDate": { "shape": "String" }, + "updatedDate": { "shape": "String" }, + "tags": { "shape": "Tags" }, + "databaseType": { "shape": "DatabaseType" }, + "connectableResourceType": { "shape": "String" }, + "connectableResourceIdentifier": { "shape": "ResourceIdentifier" } + } + }, + "ConnectionAuthenticationTypes": { + "type": "string", + "enum": ["2", "3", "4", "5", "6", "7", "8"], + "sensitive": true + }, + "ConnectionName": { + "type": "string", + "sensitive": true + }, + "ConnectionProperties": { + "type": "map", + "key": { "shape": "ConnectionPropertyKey" }, + "value": { "shape": "ConnectionPropertyValue" }, + "max": 50, + "min": 1 + }, + "ConnectionPropertyKey": { + "type": "string", + "max": 1000, + "min": 1 + }, + "ConnectionPropertyValue": { + "type": "string", + "max": 1000, + "min": 0 + }, + "CreateConnectionRequest": { + "type": "structure", + "required": ["name", "databaseName", "authenticationType"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "name": { + "shape": "CreateConnectionRequestNameString", + "documentation": "

Name of the connection

" + }, + "databaseName": { + "shape": "CreateConnectionRequestDatabaseNameString", + "documentation": "

Name of the database used for this connection

" + }, + "authenticationType": { + "shape": "CreateConnectionRequestAuthenticationTypeEnum", + "documentation": "

Number representing the type of authentication to use (2 = IAM, 3 = Username and Password, 4 = Federated connection)

" + }, + "isProd": { "shape": "CreateConnectionRequestIsProdString" }, + "userSettings": { "shape": "UserSettings" }, + "secretArn": { + "shape": "CreateConnectionRequestSecretArnString", + "documentation": "

secretArn for redshift cluster

" + }, + "clusterId": { + "shape": "CreateConnectionRequestClusterIdString", + "documentation": "

Id of the cluster used for this connection

" + }, + "isServerless": { + "shape": "Boolean", + "documentation": "

Is serverless connection

" + }, + "dbUser": { + "shape": "DbUser", + "documentation": "

User of the database used for this connection

" + }, + "isStoreNewSecret": { "shape": "CreateConnectionRequestIsStoreNewSecretString" }, + "username": { + "shape": "DbUser", + "documentation": "

Username used in the Username_Password connection type

" + }, + "password": { + "shape": "CreateConnectionRequestPasswordString", + "documentation": "

Password of the user used for this connection

" + }, + "tags": { "shape": "Tags" }, + "host": { + "shape": "CreateConnectionRequestHostString", + "documentation": "

Host address used for creating secret for Username_Password connection type

" + }, + "secretName": { "shape": "CreateConnectionRequestSecretNameString" }, + "description": { "shape": "CreateConnectionRequestDescriptionString" }, + "databaseType": { "shape": "DatabaseType" }, + "connectableResourceIdentifier": { + "shape": "CreateConnectionRequestConnectableResourceIdentifierString", + "documentation": "

Id of the connectable resource used for this connection

" + }, + "connectableResourceType": { + "shape": "CreateConnectionRequestConnectableResourceTypeString", + "documentation": "

Type of the connectable resource used for this connection

" + } + } + }, + "CreateConnectionRequestAuthenticationTypeEnum": { + "type": "string", + "enum": ["2", "3", "4", "5", "6", "7", "8"], + "max": 1, + "min": 1, + "sensitive": true + }, + "CreateConnectionRequestClusterIdString": { + "type": "string", + "max": 63, + "min": 1 + }, + "CreateConnectionRequestConnectableResourceIdentifierString": { + "type": "string", + "max": 63, + "min": 1, + "sensitive": true + }, + "CreateConnectionRequestConnectableResourceTypeString": { + "type": "string", + "max": 63, + "min": 1 + }, + "CreateConnectionRequestDatabaseNameString": { + "type": "string", + "max": 64, + "min": 1, + "sensitive": true + }, + "CreateConnectionRequestDescriptionString": { + "type": "string", + "max": 1000, + "min": 0 + }, + "CreateConnectionRequestHostString": { + "type": "string", + "max": 1000, + "min": 0 + }, + "CreateConnectionRequestIsProdString": { + "type": "string", + "max": 1000, + "min": 0 + }, + "CreateConnectionRequestIsStoreNewSecretString": { + "type": "string", + "max": 1000, + "min": 0 + }, + "CreateConnectionRequestNameString": { + "type": "string", + "max": 512, + "min": 1, + "sensitive": true + }, + "CreateConnectionRequestPasswordString": { + "type": "string", + "max": 64, + "min": 8, + "sensitive": true + }, + "CreateConnectionRequestSecretArnString": { + "type": "string", + "max": 1000, + "min": 1 + }, + "CreateConnectionRequestSecretNameString": { + "type": "string", + "max": 1000, + "min": 0 + }, + "CreateConnectionResponse": { + "type": "structure", + "members": { + "data": { "shape": "Connection" } + } + }, + "DatabaseAuthenticationMethod": { + "type": "string", + "enum": ["USERNAME_PASSWORD", "TEMPORARY_CREDENTIALS_WITH_IAM"] + }, + "DatabaseAuthenticationMethods": { + "type": "list", + "member": { "shape": "DatabaseAuthenticationMethod" } + }, + "DatabaseAuthenticationOption": { + "type": "structure", + "required": ["connectableResourceType", "authenticationMethods"], + "members": { + "connectableResourceType": { "shape": "String" }, + "authenticationMethods": { "shape": "DatabaseAuthenticationMethods" } + } + }, + "DatabaseAuthenticationOptions": { + "type": "list", + "member": { "shape": "DatabaseAuthenticationOption" } + }, + "DatabaseConfiguration": { + "type": "structure", + "required": [ + "databaseType", + "authenticationOptions", + "connectableResourceTypes", + "sessionSupported", + "eventAcknowledgementSupported", + "appendingLimitToQuerySupported", + "queryStatsSupported" + ], + "members": { + "databaseType": { "shape": "DatabaseType" }, + "authenticationOptions": { "shape": "DatabaseAuthenticationOptions" }, + "connectableResourceTypes": { "shape": "ConnectableResourceTypes" }, + "sessionSupported": { "shape": "Boolean" }, + "eventAcknowledgementSupported": { "shape": "Boolean" }, + "appendingLimitToQuerySupported": { "shape": "Boolean" }, + "queryStatsSupported": { "shape": "Boolean" } + } + }, + "DatabaseConfigurations": { + "type": "list", + "member": { "shape": "DatabaseConfiguration" } + }, + "DatabaseConnectionAccountSettings": { + "type": "structure", + "members": { + "masterKeyArn": { "shape": "KmsKeyArn" } + } + }, + "DatabaseConnectionConfiguration": { + "type": "structure", + "required": ["id", "type", "databaseType", "connectableResourceIdentifier", "connectableResourceType"], + "members": { + "id": { "shape": "DatabaseConnectionConfigurationIdString" }, + "type": { "shape": "DatabaseIntegrationConnectionAuthenticationTypes" }, + "auth": { "shape": "DatabaseConnectionConfigurationAuth" }, + "databaseType": { "shape": "DatabaseType" }, + "connectableResourceIdentifier": { "shape": "ResourceIdentifier" }, + "connectableResourceType": { "shape": "DatabaseConnectionConfigurationConnectableResourceTypeString" }, + "database": { "shape": "DatabaseName" } + } + }, + "DatabaseConnectionConfigurationAuth": { + "type": "structure", + "members": { + "secretArn": { "shape": "SecretKeyArn" }, + "username": { "shape": "DatabaseConnectionConfigurationAuthUsernameString" }, + "password": { "shape": "DatabaseConnectionConfigurationAuthPasswordString" } + } + }, + "DatabaseConnectionConfigurationAuthPasswordString": { + "type": "string", + "max": 1000, + "min": 0, + "sensitive": true + }, + "DatabaseConnectionConfigurationAuthUsernameString": { + "type": "string", + "max": 1000, + "min": 0, + "sensitive": true + }, + "DatabaseConnectionConfigurationConnectableResourceTypeString": { + "type": "string", + "max": 50, + "min": 0 + }, + "DatabaseConnectionConfigurationIdString": { + "type": "string", + "max": 2048, + "min": 32 + }, + "DatabaseIntegrationConnectionAuthenticationTypes": { + "type": "string", + "enum": ["4", "5", "6", "8"], + "sensitive": true + }, + "DatabaseName": { + "type": "string", + "max": 150, + "min": 0, + "sensitive": true + }, + "DatabaseType": { + "type": "string", + "enum": ["REDSHIFT", "ATHENA"] + }, + "DbUser": { + "type": "string", + "max": 127, + "min": 1, + "pattern": "[a-zA-Z0-9_][a-zA-Z_0-9+.@$-]*", + "sensitive": true + }, + "DeleteConnectionRequest": { + "type": "structure", + "required": ["connectionId"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "connectionId": { + "shape": "DeleteConnectionRequestConnectionIdString", + "documentation": "

Id of connection to delete

", + "location": "uri", + "locationName": "connectionId" + } + } + }, + "DeleteConnectionRequestConnectionIdString": { + "type": "string", + "max": 1000, + "min": 1 + }, + "DeleteConnectionResponse": { + "type": "structure", + "members": {} + }, + "ErrorCode": { + "type": "string", + "enum": ["QUERY_EXECUTION_NOT_FOUND", "QUERY_EXECUTION_ACCESS_DENIED"] + }, + "ExecuteQueryRequest": { + "type": "structure", + "required": ["query", "queryExecutionType", "queryResponseDeliveryType", "maxItems"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "connectionId": { "shape": "ExecuteQueryRequestConnectionIdString" }, + "databaseType": { + "shape": "DatabaseType", + "location": "querystring", + "locationName": "databaseType" + }, + "connection": { "shape": "DatabaseConnectionConfiguration" }, + "accountSettings": { "shape": "DatabaseConnectionAccountSettings" }, + "tabId": { "shape": "ExecuteQueryRequestTabIdString" }, + "executionContext": { "shape": "ExecuteQueryRequestExecutionContextList" }, + "query": { "shape": "ExecuteQueryRequestQueryString" }, + "queryExecutionType": { "shape": "QueryExecutionType" }, + "sessionId": { "shape": "ExecuteQueryRequestSessionIdString" }, + "queryResponseDeliveryType": { "shape": "QueryResponseDeliveryType" }, + "maxItems": { "shape": "ExecuteQueryRequestMaxItemsInteger" }, + "limitQueryResults": { "shape": "ExecuteQueryRequestLimitQueryResultsInteger" }, + "isExplain": { "shape": "Boolean" }, + "ignoreHistory": { "shape": "Boolean" }, + "timeoutMillis": { "shape": "ExecuteQueryRequestTimeoutMillisInteger" } + } + }, + "ExecuteQueryRequestConnectionIdString": { + "type": "string", + "max": 2048, + "min": 32 + }, + "ExecuteQueryRequestExecutionContextList": { + "type": "list", + "member": { "shape": "ParentResource" }, + "max": 100, + "min": 0 + }, + "ExecuteQueryRequestLimitQueryResultsInteger": { + "type": "integer", + "box": true, + "max": 1000, + "min": 0 + }, + "ExecuteQueryRequestMaxItemsInteger": { + "type": "integer", + "box": true, + "max": 100, + "min": 20 + }, + "ExecuteQueryRequestQueryString": { + "type": "string", + "max": 1000000, + "min": 0, + "sensitive": true + }, + "ExecuteQueryRequestSessionIdString": { + "type": "string", + "max": 100, + "min": 0 + }, + "ExecuteQueryRequestTabIdString": { + "type": "string", + "max": 100, + "min": 1 + }, + "ExecuteQueryRequestTimeoutMillisInteger": { + "type": "integer", + "box": true, + "max": 120000, + "min": 0 + }, + "ExecuteQueryResponse": { + "type": "structure", + "required": ["queryExecutions"], + "members": { + "sessionId": { "shape": "ExecuteQueryResponseSessionIdString" }, + "queryExecutions": { "shape": "QueryExecutions" }, + "statusCode": { + "shape": "statusCode", + "location": "statusCode" + } + } + }, + "ExecuteQueryResponseSessionIdString": { + "type": "string", + "max": 100, + "min": 0 + }, + "ExportQueryResultsRequest": { + "type": "structure", + "required": ["queryExecutionId", "databaseType"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "accountSettings": { "shape": "DatabaseConnectionAccountSettings" }, + "queryExecutionId": { "shape": "ExportQueryResultsRequestQueryExecutionIdString" }, + "databaseType": { + "shape": "DatabaseType", + "location": "querystring", + "locationName": "databaseType" + }, + "fileType": { "shape": "FileType" } + } + }, + "ExportQueryResultsRequestQueryExecutionIdString": { + "type": "string", + "max": 100, + "min": 1 + }, + "ExportQueryResultsResponse": { + "type": "structure", + "required": ["queryResult", "contentType", "fileName"], + "members": { + "queryResult": { "shape": "StreamingBlob" }, + "contentType": { + "shape": "String", + "location": "header", + "locationName": "Content-Type" + }, + "fileName": { + "shape": "String", + "location": "header", + "locationName": "Content-Disposition" + } + }, + "payload": "queryResult" + }, + "FileType": { + "type": "string", + "enum": ["JSON", "CSV"] + }, + "FullQueryText": { + "type": "string", + "max": 1000000, + "min": 0, + "sensitive": true + }, + "GetConnectableResourcesRequest": { + "type": "structure", + "required": ["type", "maxItems", "databaseType"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "accountSettings": { "shape": "DatabaseConnectionAccountSettings" }, + "type": { "shape": "GetConnectableResourcesRequestTypeString" }, + "maxItems": { "shape": "GetConnectableResourcesRequestMaxItemsInteger" }, + "pageToken": { "shape": "PageToken" }, + "databaseType": { + "shape": "DatabaseType", + "location": "querystring", + "locationName": "databaseType" + } + } + }, + "GetConnectableResourcesRequestMaxItemsInteger": { + "type": "integer", + "box": true, + "max": 50, + "min": 20 + }, + "GetConnectableResourcesRequestTypeString": { + "type": "string", + "max": 150, + "min": 0 + }, + "GetConnectableResourcesResponse": { + "type": "structure", + "required": ["connectableResources"], + "members": { + "connectableResources": { "shape": "ConnectableResources" }, + "nextToken": { "shape": "String" } + } + }, + "GetConnectionRequest": { + "type": "structure", + "required": ["connectionId"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "connectionId": { + "shape": "GetConnectionRequestConnectionIdString", + "documentation": "

Id of connection to delete

", + "location": "uri", + "locationName": "connectionId" + } + } + }, + "GetConnectionRequestConnectionIdString": { + "type": "string", + "max": 1000, + "min": 1 + }, + "GetConnectionResponse": { + "type": "structure", + "members": { + "data": { "shape": "Connection" } + } + }, + "GetDatabaseConfigurationsRequest": { + "type": "structure", + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "accountSettings": { "shape": "DatabaseConnectionAccountSettings" } + } + }, + "GetDatabaseConfigurationsResponse": { + "type": "structure", + "members": { + "configurations": { "shape": "DatabaseConfigurations" } + } + }, + "GetQueryExecutionHistoryRequest": { + "type": "structure", + "required": ["queryExecutionId"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "queryExecutionId": { "shape": "GetQueryExecutionHistoryRequestQueryExecutionIdString" }, + "accountSettings": { "shape": "DatabaseConnectionAccountSettings" } + } + }, + "GetQueryExecutionHistoryRequestQueryExecutionIdString": { + "type": "string", + "max": 100, + "min": 1 + }, + "GetQueryExecutionHistoryResponse": { + "type": "structure", + "members": { + "id": { "shape": "String" }, + "querySourceId": { "shape": "String" }, + "queryStartTime": { "shape": "Long" }, + "queryEndTime": { "shape": "Long" }, + "status": { "shape": "QueryExecutionStatus" }, + "queryText": { "shape": "FullQueryText" }, + "serializedMetadata": { "shape": "SerializedMetadata" }, + "serializedQueryStats": { "shape": "SerializedQueryStats" }, + "databaseType": { "shape": "DatabaseType" } + } + }, + "GetQueryResultRequest": { + "type": "structure", + "required": ["queryExecutionId", "databaseType"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "queryExecutionId": { "shape": "GetQueryResultRequestQueryExecutionIdString" }, + "accountSettings": { "shape": "DatabaseConnectionAccountSettings" }, + "pageToken": { "shape": "PageToken" }, + "databaseType": { + "shape": "DatabaseType", + "location": "querystring", + "locationName": "databaseType" + }, + "pageSize": { "shape": "GetQueryResultRequestPageSizeInteger" } + } + }, + "GetQueryResultRequestPageSizeInteger": { + "type": "integer", + "box": true, + "min": 0 + }, + "GetQueryResultRequestQueryExecutionIdString": { + "type": "string", + "max": 100, + "min": 1 + }, + "GetQueryResultResponse": { + "type": "structure", + "members": { + "queryResult": { "shape": "QueryResult" }, + "nextToken": { "shape": "String" }, + "previousToken": { "shape": "String" } + } + }, + "GetResourcesRequest": { + "type": "structure", + "required": ["parents", "type", "maxItems"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "connectionId": { "shape": "GetResourcesRequestConnectionIdString" }, + "databaseType": { + "shape": "DatabaseType", + "location": "querystring", + "locationName": "databaseType" + }, + "connection": { "shape": "DatabaseConnectionConfiguration" }, + "accountSettings": { "shape": "DatabaseConnectionAccountSettings" }, + "parents": { "shape": "ParentResources" }, + "type": { "shape": "GetResourcesRequestTypeString" }, + "maxItems": { "shape": "GetResourcesRequestMaxItemsInteger" }, + "pageToken": { "shape": "PageToken" }, + "forceRefresh": { "shape": "Boolean" }, + "forceRefreshRecursive": { "shape": "Boolean" } + } + }, + "GetResourcesRequestConnectionIdString": { + "type": "string", + "max": 2048, + "min": 32 + }, + "GetResourcesRequestMaxItemsInteger": { + "type": "integer", + "box": true, + "max": 100, + "min": 20 + }, + "GetResourcesRequestTypeString": { + "type": "string", + "max": 150, + "min": 0 + }, + "GetResourcesResponse": { + "type": "structure", + "members": { + "resources": { "shape": "Resources" }, + "nextToken": { "shape": "String" }, + "statusCode": { + "shape": "statusCode", + "location": "statusCode" + }, + "connectionProperties": { "shape": "ConnectionProperties" } + } + }, + "GetTabStatesRequest": { + "type": "structure", + "required": ["tabId"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "accountSettings": { "shape": "DatabaseConnectionAccountSettings" }, + "tabId": { "shape": "String" } + } + }, + "GetTabStatesResponse": { + "type": "structure", + "required": ["queryExecutionStates"], + "members": { + "queryExecutionStates": { "shape": "QueryExecutionStates" }, + "sessionId": { "shape": "String" } + } + }, + "Integer": { + "type": "integer", + "box": true + }, + "InternalServerError": { + "type": "structure", + "required": ["message"], + "members": { + "message": { "shape": "String" }, + "code": { "shape": "ErrorCode" } + }, + "error": { "httpStatusCode": 500 }, + "exception": true, + "fault": true + }, + "KmsKeyArn": { + "type": "string", + "max": 1000, + "min": 0, + "pattern": "arn:.*" + }, + "ListQueryExecutionHistoryRequest": { + "type": "structure", + "required": ["maxItems"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "maxItems": { "shape": "ListQueryExecutionHistoryRequestMaxItemsInteger" }, + "accountSettings": { "shape": "DatabaseConnectionAccountSettings" }, + "pageToken": { "shape": "ListQueryExecutionHistoryRequestPageTokenString" }, + "querySourceId": { "shape": "ListQueryExecutionHistoryRequestQuerySourceIdString" }, + "databaseType": { "shape": "DatabaseType" }, + "status": { "shape": "QueryExecutionStatus" }, + "startTime": { "shape": "QueryHistoryTimestamp" }, + "endTime": { "shape": "QueryHistoryTimestamp" }, + "containsText": { "shape": "ListQueryExecutionHistoryRequestContainsTextString" } + } + }, + "ListQueryExecutionHistoryRequestContainsTextString": { + "type": "string", + "max": 100, + "min": 0 + }, + "ListQueryExecutionHistoryRequestMaxItemsInteger": { + "type": "integer", + "box": true, + "max": 100, + "min": 1 + }, + "ListQueryExecutionHistoryRequestPageTokenString": { + "type": "string", + "max": 10000, + "min": 0 + }, + "ListQueryExecutionHistoryRequestQuerySourceIdString": { + "type": "string", + "max": 100, + "min": 0 + }, + "ListQueryExecutionHistoryResponse": { + "type": "structure", + "required": ["items"], + "members": { + "items": { "shape": "QueryExecutionHistoryPreviews" }, + "nextToken": { "shape": "ListQueryExecutionHistoryResponseNextTokenString" } + } + }, + "ListQueryExecutionHistoryResponseNextTokenString": { + "type": "string", + "max": 1000, + "min": 0 + }, + "ListTagsForResourceRequest": { + "type": "structure", + "required": ["resourceArn"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "resourceArn": { + "shape": "Arn", + "location": "uri", + "locationName": "resourceArn" + } + } + }, + "ListTagsForResourceResponse": { + "type": "structure", + "required": ["tags"], + "members": { + "tags": { "shape": "Tags" } + } + }, + "Long": { + "type": "long", + "box": true + }, + "PageToken": { + "type": "string", + "max": 1000, + "min": 0 + }, + "ParentResource": { + "type": "structure", + "required": ["parentId", "parentType"], + "members": { + "parentId": { "shape": "ParentResourceParentIdString" }, + "parentType": { "shape": "ParentResourceParentTypeString" } + } + }, + "ParentResourceParentIdString": { + "type": "string", + "max": 1000, + "min": 1, + "sensitive": true + }, + "ParentResourceParentTypeString": { + "type": "string", + "max": 100, + "min": 1 + }, + "ParentResources": { + "type": "list", + "member": { "shape": "ParentResource" } + }, + "PollQueryExecutionEventsRequest": { + "type": "structure", + "required": ["queryExecutionIds", "databaseType"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "queryExecutionIds": { "shape": "PollQueryExecutionEventsRequestQueryExecutionIdsList" }, + "accountSettings": { "shape": "DatabaseConnectionAccountSettings" }, + "databaseType": { + "shape": "DatabaseType", + "location": "querystring", + "locationName": "databaseType" + }, + "ackIds": { "shape": "AckIds" } + } + }, + "PollQueryExecutionEventsRequestQueryExecutionIdsList": { + "type": "list", + "member": { "shape": "PollQueryExecutionEventsRequestQueryExecutionIdsListMemberString" }, + "max": 100, + "min": 1 + }, + "PollQueryExecutionEventsRequestQueryExecutionIdsListMemberString": { + "type": "string", + "max": 100, + "min": 1 + }, + "PollQueryExecutionEventsResponse": { + "type": "structure", + "members": { + "events": { "shape": "QueryExecutionEvents" } + } + }, + "QueryCancellationStatus": { + "type": "string", + "enum": ["CANCELLED", "DOES_NOT_EXISTS", "ALREADY_FINISHED", "CANCELLATION_FAILED"] + }, + "QueryExecution": { + "type": "structure", + "required": ["queryExecutionId"], + "members": { + "queryExecutionStatus": { "shape": "QueryExecutionStatus" }, + "queryExecutionId": { "shape": "QueryExecutionQueryExecutionIdString" }, + "queryResult": { "shape": "QueryResult" }, + "queryText": { "shape": "QueryText" } + } + }, + "QueryExecutionEvent": { + "type": "structure", + "required": ["queryExecutionEventType", "queryExecutionId"], + "members": { + "queryExecutionEventType": { "shape": "QueryExecutionEventType" }, + "queryExecutionId": { "shape": "QueryExecutionEventQueryExecutionIdString" }, + "queryExecutionStatus": { "shape": "QueryExecutionStatus" }, + "queryResult": { "shape": "QueryResult" }, + "nextToken": { "shape": "String" }, + "ackId": { "shape": "String" } + } + }, + "QueryExecutionEventQueryExecutionIdString": { + "type": "string", + "max": 100, + "min": 0 + }, + "QueryExecutionEventType": { + "type": "string", + "enum": ["QUERY_EXECUTION_STATUS", "QUERY_EXECUTION_RESULT"] + }, + "QueryExecutionEvents": { + "type": "list", + "member": { "shape": "QueryExecutionEvent" } + }, + "QueryExecutionHistoryPreview": { + "type": "structure", + "members": { + "id": { "shape": "String" }, + "querySourceId": { "shape": "String" }, + "queryStartTime": { "shape": "Long" }, + "queryEndTime": { "shape": "Long" }, + "status": { "shape": "QueryExecutionStatus" }, + "queryTextPreview": { "shape": "QueryTextPreview" }, + "serializedMetadata": { "shape": "SerializedMetadata" }, + "databaseType": { "shape": "DatabaseType" } + } + }, + "QueryExecutionHistoryPreviews": { + "type": "list", + "member": { "shape": "QueryExecutionHistoryPreview" } + }, + "QueryExecutionQueryExecutionIdString": { + "type": "string", + "max": 100, + "min": 0 + }, + "QueryExecutionState": { + "type": "structure", + "required": ["queryExecutionId", "status", "databaseType"], + "members": { + "queryExecutionId": { "shape": "String" }, + "status": { "shape": "String" }, + "databaseType": { "shape": "DatabaseType" } + } + }, + "QueryExecutionStates": { + "type": "list", + "member": { "shape": "QueryExecutionState" } + }, + "QueryExecutionStatus": { + "type": "string", + "enum": ["SCHEDULED", "RUNNING", "FAILED", "CANCELLED", "FINISHED"] + }, + "QueryExecutionType": { + "type": "string", + "enum": ["PERSIST_SESSION", "NO_SESSION"] + }, + "QueryExecutionWarning": { + "type": "structure", + "members": { + "message": { "shape": "QueryExecutionWarningMessage" }, + "level": { "shape": "QueryExecutionWarningLevel" } + } + }, + "QueryExecutionWarningLevel": { + "type": "string", + "enum": ["INFO", "WARNING"] + }, + "QueryExecutionWarningMessage": { + "type": "string", + "max": 1000, + "min": 0, + "sensitive": true + }, + "QueryExecutionWarnings": { + "type": "list", + "member": { "shape": "QueryExecutionWarning" } + }, + "QueryExecutions": { + "type": "list", + "member": { "shape": "QueryExecution" } + }, + "QueryHistoryTimestamp": { + "type": "long", + "box": true + }, + "QueryResponseDeliveryType": { + "type": "string", + "enum": ["SYNC", "ASYNC"] + }, + "QueryResult": { + "type": "structure", + "members": { + "queryExecutionStatus": { "shape": "QueryExecutionStatus" }, + "headers": { "shape": "QueryResultHeaders" }, + "rows": { "shape": "Rows" }, + "affectedRows": { "shape": "Integer" }, + "totalRowCount": { "shape": "Integer" }, + "elapsedTime": { "shape": "Long" }, + "errorMessage": { "shape": "QueryResultErrorMessage" }, + "errorPosition": { "shape": "Integer" }, + "queryResultWarningCode": { "shape": "QueryResultQueryResultWarningCodeString" }, + "warnings": { "shape": "QueryExecutionWarnings" }, + "queryExecutionId": { "shape": "String" }, + "sessionId": { "shape": "String" }, + "queryText": { "shape": "QueryText" }, + "statementType": { "shape": "StatementType" }, + "serializedMetadata": { "shape": "SerializedMetadata" }, + "connectionProperties": { "shape": "ConnectionProperties" } + } + }, + "QueryResultCellType": { + "type": "string", + "enum": ["STRING", "BOOLEAN", "INTEGER", "BIG_INTEGER", "FLOAT", "BIG_DECIMAL", "DATE", "TIME", "DATETIME"] + }, + "QueryResultCellValue": { + "type": "string", + "sensitive": true + }, + "QueryResultErrorMessage": { + "type": "string", + "max": 1000, + "min": 0, + "sensitive": true + }, + "QueryResultHeader": { + "type": "structure", + "required": ["displayName", "type"], + "members": { + "displayName": { "shape": "QueryResultHeaderDisplayName" }, + "type": { "shape": "QueryResultCellType" } + } + }, + "QueryResultHeaderDisplayName": { + "type": "string", + "sensitive": true + }, + "QueryResultHeaders": { + "type": "list", + "member": { "shape": "QueryResultHeader" } + }, + "QueryResultQueryResultWarningCodeString": { + "type": "string", + "max": 100, + "min": 0 + }, + "QueryText": { + "type": "string", + "sensitive": true + }, + "QueryTextPreview": { + "type": "string", + "max": 150, + "min": 0, + "sensitive": true + }, + "Resource": { + "type": "structure", + "required": ["displayName", "identifier", "childObjectTypes"], + "members": { + "displayName": { "shape": "ResourceDisplayName" }, + "identifier": { "shape": "ResourceIdentifier" }, + "type": { "shape": "ResourceTypeString" }, + "unavailable": { "shape": "Boolean" }, + "tooltipTranslationKey": { "shape": "ResourceTooltipTranslationKeyString" }, + "childObjectTypes": { "shape": "ChildObjectTypes" }, + "allowedActions": { "shape": "ResourceActions" }, + "resourceMetadata": { "shape": "ResourceMetadataItems" } + } + }, + "ResourceAction": { + "type": "string", + "enum": ["Drop", "Truncate", "GenerateDefinition", "GenerateSelectQuery"] + }, + "ResourceActions": { + "type": "list", + "member": { "shape": "ResourceAction" } + }, + "ResourceDisplayName": { + "type": "string", + "max": 150, + "min": 0, + "sensitive": true + }, + "ResourceIdentifier": { + "type": "string", + "max": 150, + "min": 0, + "sensitive": true + }, + "ResourceMetadata": { + "type": "structure", + "members": { + "key": { "shape": "String" }, + "value": { "shape": "String" } + } + }, + "ResourceMetadataItems": { + "type": "list", + "member": { "shape": "ResourceMetadata" } + }, + "ResourceNotFoundException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { "shape": "String" }, + "code": { "shape": "ErrorCode" } + }, + "error": { + "httpStatusCode": 404, + "senderFault": true + }, + "exception": true + }, + "ResourceTooltipTranslationKeyString": { + "type": "string", + "max": 50, + "min": 0 + }, + "ResourceTypeString": { + "type": "string", + "max": 50, + "min": 0 + }, + "Resources": { + "type": "list", + "member": { "shape": "Resource" } + }, + "Row": { + "type": "structure", + "members": { + "row": { "shape": "Columns" } + } + }, + "Rows": { + "type": "list", + "member": { "shape": "Row" } + }, + "SecretKeyArn": { + "type": "string", + "max": 1000, + "min": 0, + "pattern": "arn:.*" + }, + "SerializedMetadata": { + "type": "string", + "max": 1000000, + "min": 0, + "sensitive": true + }, + "SerializedQueryStats": { + "type": "string", + "max": 1000000, + "min": 0, + "sensitive": true + }, + "ServiceQuotaExceededException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { "shape": "String" }, + "code": { "shape": "ErrorCode" } + }, + "error": { + "httpStatusCode": 402, + "senderFault": true + }, + "exception": true + }, + "SqlworkbenchSource": { + "type": "string", + "enum": ["SUS", "RQEV2"] + }, + "StatementType": { + "type": "string", + "enum": ["DQL", "DML", "DDL", "DCL", "Utility"] + }, + "StreamingBlob": { + "type": "blob", + "streaming": true + }, + "String": { "type": "string" }, + "TagKey": { + "type": "string", + "max": 128, + "min": 1, + "pattern": "([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)" + }, + "TagKeyList": { + "type": "list", + "member": { "shape": "TagKey" }, + "max": 6500, + "min": 1 + }, + "TagResourceRequest": { + "type": "structure", + "required": ["resourceArn", "tags"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "resourceArn": { + "shape": "Arn", + "location": "uri", + "locationName": "resourceArn" + }, + "tags": { "shape": "Tags" } + } + }, + "TagResourceResponse": { + "type": "structure", + "members": {} + }, + "TagValue": { + "type": "string", + "max": 256, + "min": 0, + "pattern": "([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)" + }, + "TagrisAccessDeniedException": { + "type": "structure", + "members": { + "message": { "shape": "TagrisExceptionMessage" } + }, + "exception": true + }, + "TagrisAccountId": { + "type": "string", + "max": 12, + "min": 12 + }, + "TagrisAmazonResourceName": { + "type": "string", + "max": 1011, + "min": 1 + }, + "TagrisExceptionMessage": { + "type": "string", + "max": 2048, + "min": 0 + }, + "TagrisInternalId": { + "type": "string", + "max": 64, + "min": 0 + }, + "TagrisInternalServiceException": { + "type": "structure", + "members": { + "message": { "shape": "TagrisExceptionMessage" } + }, + "exception": true, + "fault": true + }, + "TagrisInvalidArnException": { + "type": "structure", + "members": { + "message": { "shape": "TagrisExceptionMessage" }, + "sweepListItem": { "shape": "TagrisSweepListItem" } + }, + "exception": true + }, + "TagrisInvalidParameterException": { + "type": "structure", + "members": { + "message": { "shape": "TagrisExceptionMessage" } + }, + "exception": true + }, + "TagrisPartialResourcesExistResultsException": { + "type": "structure", + "members": { + "message": { "shape": "TagrisExceptionMessage" }, + "resourceExistenceInformation": { "shape": "TagrisSweepListResult" } + }, + "exception": true + }, + "TagrisStatus": { + "type": "string", + "enum": ["ACTIVE", "NOT_ACTIVE"] + }, + "TagrisSweepList": { + "type": "list", + "member": { "shape": "TagrisSweepListItem" } + }, + "TagrisSweepListItem": { + "type": "structure", + "members": { + "TagrisAccountId": { "shape": "TagrisAccountId" }, + "TagrisAmazonResourceName": { "shape": "TagrisAmazonResourceName" }, + "TagrisInternalId": { "shape": "TagrisInternalId" }, + "TagrisVersion": { "shape": "TagrisVersion" } + } + }, + "TagrisSweepListResult": { + "type": "map", + "key": { "shape": "TagrisAmazonResourceName" }, + "value": { "shape": "TagrisStatus" } + }, + "TagrisThrottledException": { + "type": "structure", + "members": { + "message": { "shape": "TagrisExceptionMessage" } + }, + "exception": true + }, + "TagrisVerifyResourcesExistInput": { + "type": "structure", + "required": ["TagrisSweepList"], + "members": { + "TagrisSweepList": { "shape": "TagrisSweepList" } + } + }, + "TagrisVerifyResourcesExistOutput": { + "type": "structure", + "required": ["TagrisSweepListResult"], + "members": { + "TagrisSweepListResult": { "shape": "TagrisSweepListResult" } + } + }, + "TagrisVersion": { "type": "long" }, + "Tags": { + "type": "map", + "key": { "shape": "TagKey" }, + "value": { "shape": "TagValue" }, + "max": 50, + "min": 1 + }, + "ThrottlingException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { "shape": "String" }, + "code": { "shape": "ErrorCode" } + }, + "error": { + "httpStatusCode": 429, + "senderFault": true + }, + "exception": true + }, + "UntagResourceRequest": { + "type": "structure", + "required": ["resourceArn", "tagKeys"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "resourceArn": { + "shape": "Arn", + "location": "uri", + "locationName": "resourceArn" + }, + "tagKeys": { + "shape": "TagKeyList", + "location": "querystring", + "locationName": "tagKeys" + } + } + }, + "UntagResourceResponse": { + "type": "structure", + "members": {} + }, + "UpdateConnectionRequest": { + "type": "structure", + "required": ["id", "authenticationType"], + "members": { + "sqlworkbenchSource": { + "shape": "SqlworkbenchSource", + "location": "header", + "locationName": "sqlworkbench-source" + }, + "id": { + "shape": "UpdateConnectionRequestIdString", + "documentation": "

Id of the connection to update

" + }, + "name": { + "shape": "UpdateConnectionRequestNameString", + "documentation": "

Name of the connection

" + }, + "databaseName": { + "shape": "UpdateConnectionRequestDatabaseNameString", + "documentation": "

Name of the database used for this connection

" + }, + "authenticationType": { + "shape": "UpdateConnectionRequestAuthenticationTypeEnum", + "documentation": "

Number representing the type of authentication to use (2 = IAM, 3 = Username and Password, 4 = Federated connection)

" + }, + "secretArn": { + "shape": "UpdateConnectionRequestSecretArnString", + "documentation": "

secretArn for redshift cluster

" + }, + "clusterId": { + "shape": "UpdateConnectionRequestClusterIdString", + "documentation": "

Id of the cluster used for this connection

" + }, + "isServerless": { + "shape": "Boolean", + "documentation": "

Is serverless connection

" + }, + "dbUser": { + "shape": "DbUser", + "documentation": "

User of the database used for this connection

" + }, + "username": { + "shape": "DbUser", + "documentation": "

Username used in the Username_Password connection type

" + }, + "password": { + "shape": "UpdateConnectionRequestPasswordString", + "documentation": "

Password of the user used for this connection

" + }, + "host": { + "shape": "String", + "documentation": "

Host address used for creating secret for Username_Password connection type

" + }, + "databaseType": { "shape": "DatabaseType" }, + "connectableResourceIdentifier": { + "shape": "UpdateConnectionRequestConnectableResourceIdentifierString", + "documentation": "

Id of the connectable resource used for this connection

" + }, + "connectableResourceType": { + "shape": "UpdateConnectionRequestConnectableResourceTypeString", + "documentation": "

Type of the connectable resource used for this connection

" + } + } + }, + "UpdateConnectionRequestAuthenticationTypeEnum": { + "type": "string", + "enum": ["2", "3", "4", "5", "6", "7", "8"], + "max": 1, + "min": 1, + "sensitive": true + }, + "UpdateConnectionRequestClusterIdString": { + "type": "string", + "max": 63, + "min": 1 + }, + "UpdateConnectionRequestConnectableResourceIdentifierString": { + "type": "string", + "max": 63, + "min": 1, + "sensitive": true + }, + "UpdateConnectionRequestConnectableResourceTypeString": { + "type": "string", + "max": 63, + "min": 1 + }, + "UpdateConnectionRequestDatabaseNameString": { + "type": "string", + "max": 64, + "min": 1, + "sensitive": true + }, + "UpdateConnectionRequestIdString": { + "type": "string", + "max": 2048, + "min": 32 + }, + "UpdateConnectionRequestNameString": { + "type": "string", + "max": 512, + "min": 1, + "sensitive": true + }, + "UpdateConnectionRequestPasswordString": { + "type": "string", + "max": 64, + "min": 8, + "sensitive": true + }, + "UpdateConnectionRequestSecretArnString": { + "type": "string", + "max": 1000, + "min": 1 + }, + "UpdateConnectionResponse": { + "type": "structure", + "members": { + "data": { "shape": "Connection" } + } + }, + "UserSettings": { + "type": "string", + "sensitive": true + }, + "ValidationException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { "shape": "String" }, + "code": { "shape": "ErrorCode" } + }, + "error": { + "httpStatusCode": 400, + "senderFault": true + }, + "exception": true + }, + "statusCode": { + "type": "integer", + "box": true, + "max": 500, + "min": 100 + } + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/credentialExpiryHandler.ts b/packages/core/src/sagemakerunifiedstudio/shared/credentialExpiryHandler.ts new file mode 100644 index 00000000000..e5169207976 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/credentialExpiryHandler.ts @@ -0,0 +1,33 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { isCredentialExpirationError } from './smusUtils' +import { SmusAuthenticationProvider } from '../auth/providers/smusAuthenticationProvider' + +/** + * + * If the provided error indicates expired credentials, it marks the connection as invalid. + * This refreshes the SmusAuthInfo node to reflect the updated authentication state. + * + * @param err The error + * @param showError If true, shows error message to user. If false, silently handles the error. + */ +export async function handleCredExpiredError(err: any, showError: boolean = false): Promise { + const errorMessage = (err as Error).message + if (isCredentialExpirationError(err)) { + if (showError) { + void vscode.window.showErrorMessage( + 'Connection to SageMaker Unified Studio has expired. Please try again after reauthentication.' + ) + } + const smusAuthProvider = SmusAuthenticationProvider.fromContext() + await smusAuthProvider.invalidateConnection() + smusAuthProvider.dispose() + } else { + if (showError) { + void vscode.window.showErrorMessage(errorMessage) + } + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/smusUtils.ts b/packages/core/src/sagemakerunifiedstudio/shared/smusUtils.ts new file mode 100644 index 00000000000..f59da3b76e8 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/smusUtils.ts @@ -0,0 +1,669 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLogger } from '../../shared/logger/logger' +import { ToolkitError } from '../../shared/errors' +import { isSageMaker } from '../../shared/extensionUtilities' +import { getResourceMetadata } from './utils/resourceMetadataUtils' +import fetch from 'node-fetch' +import { CredentialsProvider, CredentialsProviderType } from '../../auth/providers/credentials' +import { CredentialType } from '../../shared/telemetry/telemetry' +import { AwsCredentialIdentity } from '@aws-sdk/types' + +/** + * Represents SSO instance information retrieved from DataZone + */ +export interface SsoInstanceInfo { + issuerUrl: string + ssoInstanceId: string + clientId: string + region: string +} + +/** + * Response from DataZone /sso/login endpoint + */ +interface DataZoneSsoLoginResponse { + redirectUrl: string +} + +/** + * Credential expiry time constants for SMUS providers (in milliseconds) + */ +export const SmusCredentialExpiry = { + /** Domain Execution Role (DER) credentials expiry time: 10 minutes */ + derExpiryMs: 10 * 60 * 1000, + /** Project Role credentials expiry time: 10 minutes */ + projectExpiryMs: 10 * 60 * 1000, + /** Connection credentials expiry time: 10 minutes */ + connectionExpiryMs: 10 * 60 * 1000, +} as const + +/** + * Error codes for SMUS-related operations + */ +export const SmusErrorCodes = { + /** Error code for when no active SMUS connection is available */ + NoActiveConnection: 'NoActiveConnection', + /** Error code for when API calls timeout */ + ApiTimeout: 'ApiTimeout', + /** Error code for when SMUS login fails */ + SmusLoginFailed: 'SmusLoginFailed', + /** Error code for when redeeming access token fails */ + RedeemAccessTokenFailed: 'RedeemAccessTokenFailed', + /** Error code for when connection establish fails */ + FailedAuthConnecton: 'FailedAuthConnecton', + /** Error code for when user cancels an operation */ + UserCancelled: 'UserCancelled', + /** Error code for when domain account Id is missing */ + AccountIdNotFound: 'AccountIdNotFound', + /** Error code for when resource ARN is missing */ + ResourceArnNotFound: 'ResourceArnNotFound', + /** Error code for when fails to get domain account Id */ + GetDomainAccountIdFailed: 'GetDomainAccountIdFailed', + /** Error code for when fails to get project account Id */ + GetProjectAccountIdFailed: 'GetProjectAccountIdFailed', + /** Error code for when region is missing */ + RegionNotFound: 'RegionNotFound', + /** Error code for when IAM-based domain is not found in the specified region */ + IamDomainNotFound: 'IamDomainNotFound', + /** Error code for when IAM profile is not found */ + ProfileNotFound: 'ProfileNotFound', + /** Error code for when IAM credential retrieval fails */ + CredentialRetrievalFailed: 'CredentialRetrievalFailed', + /** Error code for when IAM credential provider initialization fails */ + CredentialProviderInitFailed: 'CredentialProviderInitFailed', + /** Error code for when IAM profile type is invalid */ + InvalidProfileType: 'InvalidProfileType', + /** Error code for when IAM credential validation fails */ + IamValidationFailed: 'IamValidationFailed', + /** Error code for when sign out operation fails */ + SignOutFailed: 'SignOutFailed', + /** Error code for when domain URL format is invalid */ + InvalidDomainUrl: 'InvalidDomainUrl', + /** Error code for when connection to SMUS fails */ + FailedToConnect: 'FailedToConnect', + /** Error code for when connection is not found */ + ConnectionNotFound: 'ConnectionNotFound', + /** Error code for when connection type is invalid for the operation */ + InvalidConnectionType: 'InvalidConnectionType', + /** Error code for when no group profile is found for IAM role */ + NoGroupProfileFound: 'NoGroupProfileFound', + /** Error code for when no user profile is found for IAM principal */ + NoUserProfileFound: 'NoUserProfileFound', +} as const + +/** + * Timeout constants for SMUS API calls (in milliseconds) + */ +export const SmusTimeouts = { + /** Default timeout for API calls: 10 seconds */ + apiCallTimeoutMs: 10 * 1000, +} as const + +/** + * DataZone service ID used for filtering regions + */ +export const DataZoneServiceId = 'datazone' + +/** + * Domain version constants + */ +export const DomainVersionV1 = 'V1' +export const DomainVersionV2 = 'V2' + +/** + * IAM sign-in type constants + */ +export const IamSignInRole = 'IAM_ROLE' +export const IamSignInUser = 'IAM_USER' + +/** + * Input interface for IAM domain check function + */ +export interface IamDomainCheckInput { + domainVersion: string | undefined + iamSignIns?: string[] | undefined + domainId?: string +} + +/** + * Interface for AWS credential objects that need validation + */ +interface CredentialObject { + accessKeyId?: unknown + secretAccessKey?: unknown + sessionToken?: unknown + expiration?: unknown +} + +/** + * Validates AWS credential fields and throws appropriate errors if invalid + * @param credentials The credential object to validate + * @param errorCode The error code to use in ToolkitError + * @param contextMessage The context message for error messages (e.g., "API response", "project credential response") + * @throws ToolkitError if any credential field is invalid + */ +export function validateCredentialFields( + credentials: CredentialObject, + errorCode: string, + contextMessage: string, + validateExpireTime: boolean = false +): void { + if (!credentials.accessKeyId || typeof credentials.accessKeyId !== 'string') { + throw new ToolkitError(`Invalid accessKeyId in ${contextMessage}: ${typeof credentials.accessKeyId}`, { + code: errorCode, + }) + } + if (!credentials.secretAccessKey || typeof credentials.secretAccessKey !== 'string') { + throw new ToolkitError(`Invalid secretAccessKey in ${contextMessage}: ${typeof credentials.secretAccessKey}`, { + code: errorCode, + }) + } + if (!credentials.sessionToken || typeof credentials.sessionToken !== 'string') { + throw new ToolkitError(`Invalid sessionToken in ${contextMessage}: ${typeof credentials.sessionToken}`, { + code: errorCode, + }) + } + if (validateExpireTime) { + if (!credentials.expiration || !(credentials.expiration instanceof Date)) { + throw new ToolkitError(`Invalid expireTime in ${contextMessage}: ${typeof credentials.expiration}`, { + code: errorCode, + }) + } + } +} + +/** + * Utility class for SageMaker Unified Studio domain URL parsing and validation + */ +export class SmusUtils { + private static readonly logger = getLogger('smus') + + /** + * Extracts the domain ID from a SageMaker Unified Studio domain URL + * @param domainUrl The SageMaker Unified Studio domain URL + * @returns The extracted domain ID or undefined if not found + */ + public static extractDomainIdFromUrl(domainUrl: string): string | undefined { + try { + // Domain URL format: https://dzd_d3hr1nfjbtwui1.sagemaker.us-east-2.on.aws + const url = new URL(domainUrl) + const hostname = url.hostname + + // Extract domain ID from hostname (dzd_d3hr1nfjbtwui1 or dzd-d3hr1nfjbtwui1) + const domainIdMatch = hostname.match(/^(dzd[-_][a-zA-Z0-9_-]{1,36})\./) + return domainIdMatch?.[1] + } catch (error) { + this.logger.error('Failed to extract domain ID from URL: %s', error as Error) + return undefined + } + } + + /** + * Extracts the AWS region from a SageMaker Unified Studio domain URL + * @param domainUrl The SageMaker Unified Studio domain URL + * @param fallbackRegion Fallback region if extraction fails (default: 'us-east-1') + * @returns The extracted AWS region or the fallback region if not found + */ + public static extractRegionFromUrl(domainUrl: string, fallbackRegion: string = 'us-east-1'): string { + try { + // Domain URL formats: + // - https://dzd_d3hr1nfjbtwui1.sagemaker.us-east-2.on.aws + // - https://dzd_4gickdfsxtoxg0.sagemaker-gamma.us-west-2.on.aws + const url = new URL(domainUrl) + const hostname = url.hostname + + // Extract region from hostname, handling both prod and non-prod stages + // Pattern matches: .sagemaker[-stage].{region}.on.aws + const regionMatch = hostname.match(/\.sagemaker(?:-[a-z]+)?\.([a-z0-9-]+)\.on\.aws$/) + return regionMatch?.[1] || fallbackRegion + } catch (error) { + this.logger.error('Failed to extract region from URL: %s', error as Error) + return fallbackRegion + } + } + + /** + * Extracts both domain ID and region from a SageMaker Unified Studio domain URL + * @param domainUrl The SageMaker Unified Studio domain URL + * @param fallbackRegion Fallback region if extraction fails (default: 'us-east-1') + * @returns Object containing domainId and region + */ + public static extractDomainInfoFromUrl( + domainUrl: string, + fallbackRegion: string = 'us-east-1' + ): { domainId: string | undefined; region: string } { + return { + domainId: this.extractDomainIdFromUrl(domainUrl), + region: this.extractRegionFromUrl(domainUrl, fallbackRegion), + } + } + + /** + * Validates the domain URL format for SageMaker Unified Studio + * @param value The URL to validate + * @returns Error message if invalid, undefined if valid + */ + public static validateDomainUrl(value: string): string | undefined { + if (!value || value.trim() === '') { + return 'Domain URL is required' + } + + const trimmedValue = value.trim() + + // Check HTTPS requirement + if (!trimmedValue.startsWith('https://')) { + return 'Domain URL must use HTTPS (https://)' + } + + // Check basic URL format + try { + const url = new URL(trimmedValue) + + // Check if it looks like a SageMaker Unified Studio domain + if (!url.hostname.includes('sagemaker') || !url.hostname.includes('on.aws')) { + return 'URL must be a valid SageMaker Unified Studio domain (e.g., https://dzd_xxxxxxxxx.sagemaker.us-east-1.on.aws)' + } + + // Extract domain ID to validate + const domainId = this.extractDomainIdFromUrl(trimmedValue) + + if (!domainId) { + return 'URL must contain a valid domain ID (starting with dzd- or dzd_)' + } + + return undefined // Valid + } catch (err) { + return 'Invalid URL format' + } + } + + /** + * Makes HTTP call to DataZone /sso/login endpoint + * @param domainUrl The SageMaker Unified Studio domain URL + * @param domainId The extracted domain ID + * @returns Promise resolving to the login response + * @throws ToolkitError if the API call fails + */ + private static async callDataZoneLogin(domainUrl: string, domainId: string): Promise { + const loginUrl = new URL('/sso/login', domainUrl) + const requestBody = { + domainId: domainId, + } + + try { + const response = await fetch(loginUrl.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'aws-toolkit-vscode', + }, + body: JSON.stringify(requestBody), + timeout: SmusTimeouts.apiCallTimeoutMs, + }) + + if (!response.ok) { + throw new ToolkitError(`SMUS login failed: ${response.status} ${response.statusText}`, { + code: SmusErrorCodes.SmusLoginFailed, + }) + } + + return (await response.json()) as DataZoneSsoLoginResponse + } catch (error) { + // Handle timeout errors specifically + if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('timeout'))) { + throw new ToolkitError( + `DataZone login request timed out after ${SmusTimeouts.apiCallTimeoutMs / 1000} seconds`, + { + code: SmusErrorCodes.ApiTimeout, + cause: error, + } + ) + } + // Re-throw other errors as-is + throw error + } + } + + /** + * Gets SSO instance information by calling DataZone /sso/login endpoint + * This extracts the proper SSO instance ID and issuer URL needed for OAuth client registration + * + * @param domainUrl The SageMaker Unified Studio domain URL + * @returns Promise resolving to SSO instance information + * @throws ToolkitError if the API call fails or response is invalid + */ + public static async getSsoInstanceInfo(domainUrl: string): Promise { + try { + this.logger.info(`Getting SSO instance info from DataZone for domainurl: ${domainUrl}`) + + // Extract domain ID from the domain URL + const domainId = this.extractDomainIdFromUrl(domainUrl) + if (!domainId) { + throw new ToolkitError('Invalid domain URL format', { code: 'InvalidDomainUrl' }) + } + + // Call DataZone /sso/login endpoint to get redirect URL with SSO instance info + const loginData = await this.callDataZoneLogin(domainUrl, domainId) + if (!loginData.redirectUrl) { + throw new ToolkitError('No redirect URL received from DataZone login', { code: 'InvalidLoginResponse' }) + } + + // Parse the redirect URL to extract SSO instance information + const redirectUrl = new URL(loginData.redirectUrl) + const clientIdParam = redirectUrl.searchParams.get('client_id') + if (!clientIdParam) { + throw new ToolkitError('No client_id found in DataZone redirect URL', { code: 'InvalidRedirectUrl' }) + } + + // Decode the client_id ARN: arn:aws:sso::785498918019:application/ssoins-6684636af7e1a207/apl-5f60548b7f5677a2 + const decodedClientId = decodeURIComponent(clientIdParam) + const arnParts = decodedClientId.split('/') + if (arnParts.length < 2) { + throw new ToolkitError('Invalid client_id ARN format', { code: 'InvalidArnFormat' }) + } + + const ssoInstanceId = arnParts[1] // Extract ssoins-6684636af7e1a207 + const issuerUrl = `https://identitycenter.amazonaws.com/${ssoInstanceId}` + + // Extract region from domain URL + const region = this.extractRegionFromUrl(domainUrl) + + this.logger.info('Extracted SSO instance info: %s', ssoInstanceId) + + return { + issuerUrl, + ssoInstanceId, + clientId: decodedClientId, + region, + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unknown error' + this.logger.error('Failed to get SSO instance info: %s', errorMsg) + + if (error instanceof ToolkitError) { + throw error + } + + throw new ToolkitError(`Failed to get SSO instance info: ${errorMsg}`, { + code: 'SsoInstanceInfoFailed', + cause: error instanceof Error ? error : undefined, + }) + } + } + /** + * Extracts SSO ID from a user ID in the format "user-" + * @param userId The user ID to extract SSO ID from + * @returns The extracted SSO ID + * @throws Error if the userId format is invalid + */ + public static extractSSOIdFromUserId(userId: string): string { + const match = userId.match(/user-(.+)$/) + if (!match) { + this.logger.error(`Invalid UserId format: ${userId}`) + throw new Error(`Invalid UserId format: ${userId}`) + } + return match[1] + } + + /** + * Checks if we're in SMUS space environment (should hide certain UI elements) + * @returns True if in SMUS space environment with DataZone domain ID + */ + public static isInSmusSpaceEnvironment(): boolean { + const isSMUSspace = isSageMaker('SMUS') || isSageMaker('SMUS-SPACE-REMOTE-ACCESS') + const resourceMetadata = getResourceMetadata() + return isSMUSspace && !!resourceMetadata?.AdditionalMetadata?.DataZoneDomainId + } + + /** + * Extracts the session name from an assumed role ARN. + * + * Note: This function ONLY works for assumed role ARNs (arn:aws:sts::*:assumed-role/*). + * It will return undefined for other IAM principal types such as: + * - IAM users (arn:aws:iam::*:user/*) + * - IAM roles (arn:aws:iam::*:role/*) + * + * @param arn The assumed role ARN (format: arn:aws:sts::ACCOUNT:assumed-role/ROLE_NAME/SESSION_NAME) + * @returns The session name if the ARN is a valid assumed role ARN, undefined otherwise + */ + public static extractSessionNameFromArn(arn: string): string | undefined { + try { + // Expected format: arn:aws:sts::ACCOUNT:assumed-role/ROLE_NAME/SESSION_NAME + const parts = arn.split(':') + if (parts.length < 6) { + return undefined + } + + // The resource part is after the 5th colon + const resourcePart = parts.slice(5).join(':') + + // Split by '/' to get assumed-role, ROLE_NAME, and SESSION_NAME + const resourceParts = resourcePart.split('/') + if (resourceParts.length < 3 || resourceParts[0] !== 'assumed-role') { + return undefined + } + + // Session name is the last part + return resourceParts[2] + } catch (err) { + return undefined + } + } + + /** + * Determines if an ARN represents an IAM user (vs IAM role session) + * @param arn The ARN to check (format: arn:aws:iam::ACCOUNT:user/USER_NAME for IAM users, + * arn:aws:sts::ACCOUNT:assumed-role/ROLE_NAME/SESSION_NAME for role sessions) + * @returns True if the ARN is an IAM user, false otherwise + */ + public static isIamUserArn(arn: string | undefined): boolean { + if (!arn) { + return false + } + + // IAM user ARN format: arn:aws:iam::ACCOUNT:user/USER_NAME + // IAM role session ARN format: arn:aws:sts::ACCOUNT:assumed-role/ROLE_NAME/SESSION_NAME + return arn.includes(':iam::') && arn.includes(':user/') + } + + /** + * Converts an STS assumed-role ARN to its corresponding IAM role ARN, or returns IAM user ARN as-is. + * Supports all AWS partitions (aws, aws-cn, aws-us-gov, etc.) + * Examples: + * Input: arn:aws:sts::123456789012:assumed-role/MyRole/MySession + * Output: arn:aws:iam::123456789012:role/MyRole + * + * Input: arn:aws:iam::123456789012:user/MyUser + * Output: arn:aws:iam::123456789012:user/MyUser + * + * Input: arn:aws-cn:sts::123456789012:assumed-role/MyRole/MySession + * Output: arn:aws-cn:iam::123456789012:role/MyRole + */ + public static convertAssumedRoleArnToIamRoleArn(stsArn: string): string { + // Check if it's already an IAM user ARN - return as-is + // Supports all AWS partitions: aws, aws-cn, aws-us-gov, etc. + const iamUserRegex = /^arn:(aws[a-z-]*):iam::(\d{12}):user\/([A-Za-z0-9+=,.@_\/-]+)$/ + if (iamUserRegex.test(stsArn)) { + return stsArn + } + + // Check if it's already an IAM role ARN - return as-is + const iamRoleRegex = /^arn:(aws[a-z-]*):iam::(\d{12}):role\/([A-Za-z0-9+=,.@_\/-]+)$/ + if (iamRoleRegex.test(stsArn)) { + return stsArn + } + + // Try to convert STS assumed-role ARN to IAM role ARN + const arnRegex = /^arn:(aws[a-z-]*):sts::(\d{12}):assumed-role\/([A-Za-z0-9+=,.@_\/-]+)\/([A-Za-z0-9+=,.@_-]+)$/ + const match = stsArn.match(arnRegex) + if (!match) { + throw new Error(`Invalid STS ARN format: ${stsArn}`) + } + + const [, partition, accountId, roleName] = match + + return `arn:${partition}:iam::${accountId}:role/${roleName}` + } +} + +/** + * Determines if a domain is an IAM domain based on IamSignIns field. + * + * IAM domains are V2 domains that support both IAM role and IAM user authentication. + * A domain is considered an IAM domain if its IamSignIns array contains both: + * - IAM_ROLE + * - IAM_USER + * + * @param input - Object containing domain version, IamSignIns, and optional domainId for logging + * @returns true if the domain is an IAM domain, false otherwise + */ +export function isIamDomain(input: IamDomainCheckInput): boolean { + const logger = getLogger('smus') + const domainIdLog = input.domainId ? ` for domain ${input.domainId}` : '' + + // Only V2 domains can be IAM domains + if (input.domainVersion !== DomainVersionV2) { + logger.debug( + `IAM domain check${domainIdLog}: Domain version is not V2 (value: ${input.domainVersion}), returning false` + ) + return false + } + + // Check if IamSignIns contains both IAM_ROLE and IAM_USER + if (!input.iamSignIns || !Array.isArray(input.iamSignIns)) { + logger.debug(`IAM domain check${domainIdLog}: IamSignIns is missing or invalid, returning false`) + return false + } + + const hasIamRole = input.iamSignIns.includes(IamSignInRole) + const hasIamUser = input.iamSignIns.includes(IamSignInUser) + + if (hasIamRole && hasIamUser) { + logger.debug(`IAM domain check${domainIdLog}: IAM domain detected via IamSignIns`) + return true + } + + logger.debug( + `IAM domain check${domainIdLog}: IamSignIns does not contain both IAM_ROLE and IAM_USER, returning false` + ) + return false +} + +/** + * Extracts the account ID from a SageMaker ARN. + * Supports formats like: + * arn:aws:sagemaker:::app/* + * + * @param arn - The full SageMaker ARN string + * @returns The account ID from the ARN + * @throws If the ARN format is invalid + */ +export function extractAccountIdFromSageMakerArn(arn: string): string { + // Match the ARN components to extract account ID + const regex = /^arn:aws:sagemaker:(?[^:]+):(?\d+):(app|space)\/.+$/i + const match = arn.match(regex) + + if (!match?.groups) { + throw new ToolkitError(`Invalid SageMaker ARN format: "${arn}"`) + } + + return match.groups.accountId +} + +/** + * Extracts account ID from ResourceArn in SMUS space environment + * @returns Promise resolving to the account ID + * @throws ToolkitError if unable to extract account ID + */ +export async function extractAccountIdFromResourceMetadata(): Promise { + const logger = getLogger('smus') + + try { + logger.debug('Extracting account ID from ResourceArn in resource-metadata file') + + const resourceMetadata = getResourceMetadata()! + const resourceArn = resourceMetadata.ResourceArn + + if (!resourceArn) { + throw new Error('ResourceArn not found in metadata file') + } + + const accountId = extractAccountIdFromSageMakerArn(resourceArn) + logger.debug(`Successfully extracted account ID from resource-metadata file: ${accountId}`) + + return accountId + } catch (err) { + logger.error(`Failed to extract account ID from ResourceArn: %s`, err) + throw new Error('Failed to extract AWS account ID from ResourceArn in SMUS space environment') + } +} + +/** + * Creates a CredentialsProvider from an AWS credentials function + * @param credentialsFunction Function that returns AWS credentials + * @param credentialTypeId Identifier for the credential type + * @param hashCode Unique hash code for caching + * @param region Domain region + * @returns Complete CredentialsProvider object + */ +export function convertToToolkitCredentialProvider( + credentialsFunction: () => Promise, + credentialTypeId: string, + hashCode: string, + region: string +): CredentialsProvider { + return { + getCredentials: credentialsFunction, + getCredentialsId: () => ({ credentialSource: 'temp' as const, credentialTypeId }), + getProviderType: () => 'temp' as CredentialsProviderType, + getTelemetryType: () => 'other' as CredentialType, + getDefaultRegion: () => region, + getHashCode: () => hashCode, + canAutoConnect: () => Promise.resolve(false), + isAvailable: () => Promise.resolve(true), + } +} + +/** + * Checks if an error indicates credential/token expiration + * + * @param error The error to check (can be any type) + * @returns true if the error indicates expired credentials, false otherwise + * + */ +export function isCredentialExpirationError(error: any): boolean { + if (!error) { + return false + } + + const errorName = (error.name || '') as string + const errorMessage = (error.message || '') as string + const errorNameLower = errorName.toLowerCase() + const errorMessageLower = errorMessage.toLowerCase() + + const expirationErrorNames = ['ExpiredTokenException'] + + const expirationErrorMessages = ['The security token included in the request is expired'] + + // Return true if error name matches any expiration error names (case-insensitive) + if (expirationErrorNames.some((name) => name.toLowerCase() === errorNameLower)) { + return true + } + + // Return true if error message contains any expiration error names (case-insensitive) + if (expirationErrorNames.some((errorName) => errorMessageLower.includes(errorName.toLowerCase()))) { + return true + } + + // Return true if error message contains any expiration error messages + if (expirationErrorMessages.some((keyword) => errorMessageLower.includes(keyword.toLowerCase()))) { + return true + } + + return false +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/telemetry.ts b/packages/core/src/sagemakerunifiedstudio/shared/telemetry.ts new file mode 100644 index 00000000000..ff518ec3b8e --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/telemetry.ts @@ -0,0 +1,128 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + SmusLogin, + SmusOpenRemoteConnection, + SmusRenderLakehouseNode, + SmusRenderS3Node, + SmusSignOut, + SmusStopSpace, + Span, +} from '../../shared/telemetry/telemetry' +import { SagemakerUnifiedStudioSpaceNode } from '../explorer/nodes/sageMakerUnifiedStudioSpaceNode' +import { SageMakerUnifiedStudioSpacesParentNode } from '../explorer/nodes/sageMakerUnifiedStudioSpacesParentNode' +import { SmusAuthenticationProvider } from '../auth/providers/smusAuthenticationProvider' +import { getLogger } from '../../shared/logger/logger' +import { getContext } from '../../shared/vscode/setContext' +import { ConnectionCredentialsProvider } from '../auth/providers/connectionCredentialsProvider' +import { DataZoneConnection } from './client/datazoneClient' +import { createDZClientBaseOnDomainMode } from '../explorer/nodes/utils' + +/** + * Records space telemetry + */ +export async function recordSpaceTelemetry( + span: Span | Span, + node: SagemakerUnifiedStudioSpaceNode +) { + const logger = getLogger('smus') + + try { + const parent = node.resource.getParent() as SageMakerUnifiedStudioSpacesParentNode + const authProvider = SmusAuthenticationProvider.fromContext() + const accountId = await authProvider.getDomainAccountId() + const projectId = parent?.getProjectId() + + // Get project account ID and region + let projectAccountId: string | undefined + let projectRegion: string | undefined + + if (projectId) { + projectAccountId = await authProvider.getProjectAccountId(projectId) + + // Get project region from tooling environment + const dzClient = await createDZClientBaseOnDomainMode(authProvider) + const toolingEnv = await dzClient.getToolingEnvironment(projectId) + projectRegion = toolingEnv.awsAccountRegion + } + + span.record({ + smusAuthMode: authProvider.activeConnection?.type, + smusSpaceKey: node.resource.DomainSpaceKey, + smusDomainRegion: node.resource.regionCode, + smusDomainId: parent?.getAuthProvider()?.getDomainId(), + smusDomainAccountId: accountId, + smusProjectId: projectId, + smusProjectAccountId: projectAccountId, + smusProjectRegion: projectRegion, + }) + } catch (err) { + logger.error(`Failed to record space telemetry: ${(err as Error).message}`) + } +} + +/** + * Records auth telemetry + */ +export async function recordAuthTelemetry( + span: Span | Span, + authProvider: SmusAuthenticationProvider, + domainId: string | undefined, + region: string | undefined +) { + const logger = getLogger('smus') + + span.record({ + smusAuthMode: authProvider.activeConnection?.type, + smusDomainId: domainId, + awsRegion: region, + }) + + try { + if (!region) { + throw new Error(`Region is undefined for domain ${domainId}`) + } + const accountId = await authProvider.getDomainAccountId() + span.record({ + smusDomainAccountId: accountId, + }) + } catch (err) { + logger.error( + `Failed to record Domain AccountId in data connection telemetry for domain ${domainId} in region ${region}: ${err}` + ) + } +} + +/** + * Records data connection telemetry for SMUS nodes + */ +export async function recordDataConnectionTelemetry( + span: Span | Span, + connection: DataZoneConnection, + connectionCredentialsProvider: ConnectionCredentialsProvider +) { + const logger = getLogger('smus') + + try { + const isInSmusSpace = getContext('aws.smus.inSmusSpaceEnvironment') + const authProvider = SmusAuthenticationProvider.fromContext() + const accountId = await connectionCredentialsProvider.getDomainAccountId() + + span.record({ + smusAuthMode: authProvider.activeConnection?.type, + smusToolkitEnv: isInSmusSpace ? 'smus_space' : 'local', + smusDomainId: connection.domainId, + smusDomainAccountId: accountId, + smusProjectId: connection.projectId, + smusConnectionId: connection.connectionId, + smusConnectionType: connection.type, + smusProjectRegion: connection.location?.awsRegion, + smusProjectAccountId: connection.location?.awsAccountId, + }) + } catch (err) { + logger.error(`Failed to record data connection telemetry: ${(err as Error).message}`) + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/utils/resourceMetadataUtils.ts b/packages/core/src/sagemakerunifiedstudio/shared/utils/resourceMetadataUtils.ts new file mode 100644 index 00000000000..29c372b7968 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/utils/resourceMetadataUtils.ts @@ -0,0 +1,93 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fs } from '../../../shared/fs/fs' +import { getLogger } from '../../../shared/logger/logger' +import { isSageMaker } from '../../../shared/extensionUtilities' + +/** + * Resource metadata schema used by `resource-metadata.json` in SageMaker Unified Studio spaces + */ +export type ResourceMetadata = { + AppType?: string + DomainId?: string + SpaceName?: string + UserProfileName?: string + ExecutionRoleArn?: string + ResourceArn?: string + ResourceName?: string + AppImageVersion?: string + AdditionalMetadata?: { + DataZoneDomainId?: string + DataZoneDomainRegion?: string + DataZoneEndpoint?: string + DataZoneEnvironmentId?: string + DataZoneProjectId?: string + DataZoneScopeName?: string + DataZoneStage?: string + DataZoneUserId?: string + PrivateSubnets?: string + ProjectS3Path?: string + SecurityGroup?: string + } + ResourceArnCaseSensitive?: string + IpAddressType?: string +} & Record + +const resourceMetadataPath = '/opt/ml/metadata/resource-metadata.json' +let resourceMetadata: ResourceMetadata | undefined = undefined + +/** + * Gets the cached resource metadata (must be initialized with `initializeResourceMetadata()` first) + * @returns ResourceMetadata object or undefined if not yet initialized + */ +export function getResourceMetadata(): ResourceMetadata | undefined { + return resourceMetadata +} + +/** + * Initializes resource metadata by reading and parsing the resource-metadata.json file + */ +export async function initializeResourceMetadata(): Promise { + const logger = getLogger('smus') + + if (!isSageMaker('SMUS') && !isSageMaker('SMUS-SPACE-REMOTE-ACCESS')) { + logger.debug(`Not in SageMaker Unified Studio space, skipping initialization of resource metadata`) + return + } + + try { + if (!(await resourceMetadataFileExists())) { + logger.debug(`Resource metadata file not found at: ${resourceMetadataPath}`) + } + + const fileContent = await fs.readFileText(resourceMetadataPath) + resourceMetadata = JSON.parse(fileContent) as ResourceMetadata + logger.debug(`Successfully read resource metadata from: ${resourceMetadataPath}`) + } catch (error) { + logger.error(`Failed to read or parse resource metadata file: ${error as Error}`) + } +} + +/** + * Checks if the resource-metadata.json file exists + * @returns True if the file exists, false otherwise + */ +export async function resourceMetadataFileExists(): Promise { + try { + return await fs.existsFile(resourceMetadataPath) + } catch (error) { + const logger = getLogger('smus') + logger.error(`Failed to check if resource metadata file exists: ${error as Error}`) + return false + } +} + +/** + * Resets the cached resource metadata + */ +export function resetResourceMetadata(): void { + resourceMetadata = undefined +} diff --git a/packages/core/src/sagemakerunifiedstudio/uriHandlers.ts b/packages/core/src/sagemakerunifiedstudio/uriHandlers.ts new file mode 100644 index 00000000000..ec313d50152 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/uriHandlers.ts @@ -0,0 +1,132 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { SearchParams } from '../shared/vscode/uriHandler' +import { ExtContext } from '../shared/extensions' +import { deeplinkConnect } from '../awsService/sagemaker/commands' +import { telemetry } from '../shared/telemetry/telemetry' +import { SmusAuthMode } from '../shared/telemetry/telemetry.gen' +/** + * Registers the SMUS deeplink URI handler at path `/connect/smus`. + * + * This handler processes deeplink URLs from the SageMaker Unified Studio console + * to establish remote connections to SMUS spaces. + * + * @param ctx Extension context containing the URI handler + * @returns Disposable for cleanup + */ +export function register(ctx: ExtContext) { + async function connectHandler(params: ReturnType) { + await telemetry.smus_deeplinkConnect.run(async (span) => { + span.record(extractTelemetryMetadata(params)) + + // WORKAROUND: The ws_url from the startSession API call contains a query parameter + // 'cell-number' within itself. When the entire deeplink URL is processed by the URI + // handler, 'cell-number' is parsed as a standalone query parameter at the top level + // instead of remaining part of the ws_url. This causes the ws_url to lose the + // cell-number context it needs. To fix this, we manually re-append the cell-number + // query parameter back to the ws_url to restore the original intended URL structure. + await deeplinkConnect( + ctx, + params.connection_identifier, + params.session, + `${params.ws_url}&cell-number=${params['cell-number']}`, // Re-append cell-number to ws_url + params.token, + params.domain, + params.app_type, + undefined, + undefined, + undefined, + true // isSMUS=true for SMUS connections + ) + }) + } + + return vscode.Disposable.from(ctx.uriHandler.onPath('/connect/smus', connectHandler, parseConnectParams)) +} + +/** + * Parses and validates SMUS deeplink URI parameters. + * + * Required parameters: + * - connection_identifier: Space ARN identifying the SMUS space + * - domain: Domain ID for the SMUS space (SM AI side) + * - user_profile: User profile name + * - session: SSM session ID + * - ws_url: WebSocket URL for SSM connection (originally contains cell-number as a query param) + * - cell-number: extracted from ws_url during URI parsing + * - token: Authentication token + * + * Optional parameters: + * - app_type: Application type (e.g., JupyterLab, CodeEditor) + * - smus_domain_id: SMUS domain identifier + * - smus_domain_account_id: SMUS domain account ID + * - smus_project_id: SMUS project identifier + * - smus_domain_region: SMUS domain region + * - smus_auth_mode: Authentication mode (sso or iam) + * + * Note: The ws_url from startSession API originally includes cell-number as a query parameter. + * However, when the deeplink URL is processed, the URI handler extracts cell-number as a + * separate top-level parameter. This is why we need to re-append it in the connectHandler. + * + * @param query URI query parameters + * @returns Parsed parameters object + * @throws Error if required parameters are missing + */ +export function parseConnectParams(query: SearchParams) { + const requiredParams = query.getFromKeysOrThrow( + 'connection_identifier', + 'domain', + 'user_profile', + 'session', + 'ws_url', + 'cell-number', + 'token' + ) + const optionalParams = query.getFromKeys( + 'app_type', + 'smus_domain_id', + 'smus_domain_account_id', + 'smus_project_id', + 'smus_domain_region', + 'smus_auth_mode' + ) + + return { ...requiredParams, ...optionalParams } +} + +/** + * Extracts telemetry metadata from URI parameters and space ARN. + * + * @param params Parsed URI parameters + * @returns Telemetry metadata object + */ +function extractTelemetryMetadata(params: ReturnType) { + // Extract metadata from space ARN + // ARN format: arn:aws:sagemaker:region:account-id:space/domain-id/space-name + const arnParts = params.connection_identifier.split(':') + const resourceParts = arnParts[5]?.split('/') // Gets "space/domain-id/space-name" + + const projectRegion = arnParts[3] // region from ARN + const projectAccountId = arnParts[4] // account-id from ARN + const domainIdFromArn = resourceParts?.[1] // domain-id from ARN + const spaceName = resourceParts?.[2] // space-name from ARN + + // Validate and cast smusAuthMode to the expected type + const authMode = params.smus_auth_mode + const smusAuthMode: SmusAuthMode | undefined = authMode === 'sso' || authMode === 'iam' ? authMode : undefined + + return { + smusDomainId: params.smus_domain_id, + smusDomainAccountId: params.smus_domain_account_id, + smusProjectId: params.smus_project_id, + smusDomainRegion: params.smus_domain_region, + smusProjectRegion: projectRegion, + smusProjectAccountId: projectAccountId, + smusSpaceKey: domainIdFromArn && spaceName ? `${domainIdFromArn}/${spaceName}` : undefined, + smusAuthMode: smusAuthMode, + } +} diff --git a/packages/core/src/shared/activationReloadState.ts b/packages/core/src/shared/activationReloadState.ts index 70d236d2dd0..bcdefa925f3 100644 --- a/packages/core/src/shared/activationReloadState.ts +++ b/packages/core/src/shared/activationReloadState.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Runtime } from 'aws-sdk/clients/lambda' +import { Runtime } from '@aws-sdk/client-lambda' import globals from './extensionGlobals' export interface SamInitState { @@ -22,7 +22,7 @@ export class ActivationReloadState { return { template: globals.globalState.get('ACTIVATION_TEMPLATE_PATH_KEY'), readme: globals.globalState.get('ACTIVATION_LAUNCH_PATH_KEY'), - runtime: globals.globalState.get('SAM_INIT_RUNTIME_KEY'), + runtime: globals.globalState.get('SAM_INIT_RUNTIME_KEY'), architecture: globals.globalState.get('SAM_INIT_ARCH_KEY'), isImage: globals.globalState.get('SAM_INIT_IMAGE_BOOLEAN_KEY'), } diff --git a/packages/core/src/shared/awsClientBuilder.ts b/packages/core/src/shared/awsClientBuilder.ts index bdec40957cb..849bdeeabd7 100644 --- a/packages/core/src/shared/awsClientBuilder.ts +++ b/packages/core/src/shared/awsClientBuilder.ts @@ -10,6 +10,7 @@ import { AwsContext } from './awsContext' import { DevSettings } from './settings' import { getUserAgent } from './telemetry/util' import { telemetry } from './telemetry/telemetry' +import { isLocalStackConnection } from '../auth/utils' /** Suppresses a very noisy warning printed by AWS SDK v2, which clutters local debugging output, CI logs, etc. */ export function disableAwsSdkWarning() { @@ -82,8 +83,11 @@ export class DefaultAWSClientBuilder implements AWSClientBuilder { const listeners = Array.isArray(onRequest) ? onRequest : [onRequest] const opt = { ...options } delete opt.onRequestSetup + if (opt.credentialProvider) { + opt.credentials = await opt.credentialProvider.resolvePromise() + } - if (!opt.credentials && !opt.token) { + if (!opt.credentials && !opt.token && !opt.credentialProvider) { const shim = this.awsContext.credentialsShim if (!shim) { @@ -141,6 +145,16 @@ export class DefaultAWSClientBuilder implements AWSClientBuilder { apiConfig?.metadata?.serviceId?.toLowerCase() ?? (type as unknown as { serviceIdentifier?: string }).serviceIdentifier + // Get endpoint url from the active profile if there's no endpoint directly passed as a parameter + const endpointUrl = this.awsContext.getCredentialEndpointUrl() + if (!('endpoint' in opt) && endpointUrl !== undefined) { + opt.endpoint = endpointUrl + } + if (isLocalStackConnection()) { + // Disable host prefixes for LocalStack + opt.hostPrefixEnabled = false + } + // Then check if there's an endpoint in the dev settings if (serviceName) { opt.endpoint = settings.get('endpoints', {})[serviceName] ?? opt.endpoint } diff --git a/packages/core/src/shared/awsClientBuilderV3.ts b/packages/core/src/shared/awsClientBuilderV3.ts index c51cc009e91..6899ce0d508 100644 --- a/packages/core/src/shared/awsClientBuilderV3.ts +++ b/packages/core/src/shared/awsClientBuilderV3.ts @@ -12,7 +12,7 @@ import { TokenIdentity, TokenIdentityProvider, } from '@smithy/types' -import { getUserAgent } from './telemetry/util' +import { getUserAgentPairs } from './telemetry/util' import { DevSettings } from './settings' import { BuildHandler, @@ -31,17 +31,18 @@ import { RetryStrategy, UserAgent, } from '@aws-sdk/types' +import { S3Client } from '@aws-sdk/client-s3' import { FetchHttpHandler } from '@smithy/fetch-http-handler' import { HttpResponse, HttpRequest } from '@aws-sdk/protocol-http' import { ConfiguredRetryStrategy } from '@smithy/util-retry' import { telemetry } from './telemetry/telemetry' import { getRequestId, getTelemetryReason, getTelemetryReasonDesc, getTelemetryResult } from './errors' -import { extensionVersion } from './vscode/env' import { getLogger } from './logger/logger' import { partialClone } from './utilities/collectionUtils' import { selectFrom } from './utilities/tsUtils' import { once } from './utilities/functionUtils' import { isWeb } from './extensionGlobals' +import { isLocalStackConnection } from '../auth/utils' export type AwsClientConstructor = new (o: AwsClientOptions) => C export type AwsCommandConstructor> = new ( @@ -70,7 +71,7 @@ export interface AwsCommand export interface AwsClientOptions { credentials: AwsCredentialIdentityProvider region: string | Provider - userAgent: UserAgent + customUserAgent: UserAgent requestHandler: { metadata?: RequestHandlerMetadata handle: (req: any, options?: any) => Promise> @@ -81,6 +82,8 @@ export interface AwsClientOptions { retryStrategy: RetryStrategy | RetryStrategyV2 logger: Logger token: TokenIdentity | TokenIdentityProvider + forcePathStyle: boolean + hostPrefixEnabled: boolean } interface AwsServiceOptions { @@ -125,6 +128,7 @@ export class AWSClientBuilderV3 { JSON.stringify(serviceOptions.clientOptions), serviceOptions.region, serviceOptions.userAgent ? '1' : '0', + this.context.getCredentialEndpointUrl(), // It gets the valid endpoint at the moment of creation serviceOptions.settings ? JSON.stringify(serviceOptions.settings.get('endpoints', {})) : '', ].join(':') } @@ -150,8 +154,8 @@ export class AWSClientBuilderV3 { opt.region = serviceOptions.region } - if (!opt.userAgent && userAgent) { - opt.userAgent = [[getUserAgent({ includePlatform: true, includeClientId: true }), extensionVersion]] + if (!opt.customUserAgent && userAgent) { + opt.customUserAgent = getUserAgentPairs({ includePlatform: true, includeClientId: true }) } if (!opt.retryStrategy) { @@ -173,9 +177,25 @@ export class AWSClientBuilderV3 { return creds } } - + // Get endpoint url from the active profile if there's no endpoint directly passed as a parameter + const endpointUrl = this.context.getCredentialEndpointUrl() + if (!('endpoint' in opt) && endpointUrl !== undefined) { + // Because we check that 'endpoint' doesn't exist in `opt`, TS complains when we actually add it + // @ts-expect-error TS2339 + opt.endpoint = endpointUrl + } + if (isLocalStackConnection()) { + // Disable host prefixes for LocalStack + opt.hostPrefixEnabled = false + // serviceClient name gets minified, but it's always consistent + if (serviceOptions.serviceClient.name === S3Client.name) { + // Use path-style S3 URLs for LocalStack + opt.forcePathStyle = true + } + } const service = new serviceOptions.serviceClient(opt) service.middlewareStack.add(telemetryMiddleware, { step: 'deserialize' }) + service.middlewareStack.add(captureHeadersMiddleware, { step: 'deserialize' }) service.middlewareStack.add(loggingMiddleware, { step: 'finalizeRequest' }) service.middlewareStack.add(getEndpointMiddleware(serviceOptions.settings), { step: 'build' }) @@ -234,6 +254,26 @@ function getEndpointMiddleware(settings: DevSettings = DevSettings.instance): Bu const keepAliveMiddleware: BuildMiddleware = (next: BuildHandler) => async (args: any) => addKeepAliveHeader(next, args) +/** + * Middleware that captures HTTP response headers and attaches them to the output object. + * This makes headers accessible via `response.$httpHeaders` for all AWS SDK v3 operations. + * Useful for detecting custom headers from services like LocalStack. + */ +const captureHeadersMiddleware: DeserializeMiddleware = + (next: DeserializeHandler) => async (args: any) => { + const result = await next(args) + + // Extract headers from HTTP response and attach to output for easy access + if (HttpResponse.isInstance(result.response)) { + const headers = result.response.headers + if (headers && result.output) { + result.output.$httpHeaders = headers + } + } + + return result + } + export async function emitOnRequest(next: DeserializeHandler, context: HandlerExecutionContext, args: any) { if (!HttpResponse.isInstance(args.request)) { return next(args) diff --git a/packages/core/src/shared/awsContext.ts b/packages/core/src/shared/awsContext.ts index 3d38978cbe6..9acb9e994fe 100644 --- a/packages/core/src/shared/awsContext.ts +++ b/packages/core/src/shared/awsContext.ts @@ -13,6 +13,7 @@ export interface AwsContextCredentials { readonly credentialsId: string readonly accountId?: string readonly defaultRegion?: string + readonly endpointUrl?: string } /** AWS Toolkit context change */ @@ -106,6 +107,13 @@ export class DefaultAwsContext implements AwsContext { return this.currentCredentials?.defaultRegion ?? defaultRegion } + /** + * Gets the endpoint URL configured for the current credentials profile, if any. + */ + public getCredentialEndpointUrl(): string | undefined { + return this.currentCredentials?.endpointUrl + } + private emitEvent() { // TODO(jmkeyes): skip this if the state did not actually change. this._onDidChangeContext.fire({ diff --git a/packages/core/src/shared/clients/clientWrapper.ts b/packages/core/src/shared/clients/clientWrapper.ts index a90d009eb18..beb117a9bf6 100644 --- a/packages/core/src/shared/clients/clientWrapper.ts +++ b/packages/core/src/shared/clients/clientWrapper.ts @@ -19,22 +19,13 @@ export abstract class ClientWrapper implements vscode.Dispo public constructor( public readonly regionCode: string, - private readonly clientType: AwsClientConstructor, - private readonly isSageMaker: boolean = false + private readonly clientType: AwsClientConstructor ) {} protected getClient(ignoreCache: boolean = false) { const args = { serviceClient: this.clientType, region: this.regionCode, - ...(this.isSageMaker - ? { - clientOptions: { - endpoint: `https://sagemaker.${this.regionCode}.amazonaws.com`, - region: this.regionCode, - }, - } - : {}), } return ignoreCache ? globals.sdkClientBuilderV3.createAwsService(args) diff --git a/packages/core/src/shared/clients/codecatalystClient.ts b/packages/core/src/shared/clients/codecatalystClient.ts index 2fa9f7b31a2..b7618fe01b0 100644 --- a/packages/core/src/shared/clients/codecatalystClient.ts +++ b/packages/core/src/shared/clients/codecatalystClient.ts @@ -9,7 +9,6 @@ import * as vscode from 'vscode' import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import * as AWS from 'aws-sdk' import * as logger from '../logger/logger' import { CancellationError, Timeout, waitTimeout, waitUntil } from '../utilities/timeoutUtils' import { isUserCancelledError } from '../../shared/errors' @@ -24,10 +23,8 @@ import { } from '../utilities/tsUtils' import { AsyncCollection, toCollection } from '../utilities/asyncCollection' import { joinAll, pageableToCollection } from '../utilities/collectionUtils' -import { CodeCatalyst } from 'aws-sdk' import { ToolkitError } from '../errors' import { Uri } from 'vscode' -import { GetSourceRepositoryCloneUrlsRequest } from 'aws-sdk/clients/codecatalyst' import { CodeCatalystClient as CodeCatalystSDKClient, CreateAccessTokenCommand, @@ -53,15 +50,18 @@ import { GetProjectCommandOutput, GetProjectRequest, GetSourceRepositoryCloneUrlsCommand, + GetSourceRepositoryCloneUrlsRequest, GetSourceRepositoryCloneUrlsResponse, GetSpaceCommand, GetSpaceCommandOutput, GetSpaceRequest, GetSubscriptionCommand, GetSubscriptionRequest, + GetSubscriptionResponse, GetUserDetailsCommand, GetUserDetailsCommandOutput, GetUserDetailsRequest, + GetUserDetailsResponse, ListDevEnvironmentsCommand, ListDevEnvironmentsRequest, ListDevEnvironmentsResponse, @@ -73,6 +73,7 @@ import { ListSourceRepositoriesRequest, ListSourceRepositoriesResponse, ListSourceRepositoryBranchesCommand, + ListSourceRepositoryBranchesItem, ListSourceRepositoryBranchesRequest, ListSpacesCommand, ListSpacesRequest, @@ -152,14 +153,14 @@ export interface DevEnvironment extends CodeCatalystDevEnvironmentSummary { /** CodeCatalyst developer environment session. */ // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CodeCatalystDevEnvSession extends CodeCatalyst.StartDevEnvironmentResponse {} +export interface CodeCatalystDevEnvSession extends StartDevEnvironmentResponse {} export interface CodeCatalystOrg extends SpaceSummary { readonly type: 'org' readonly name: string } -export interface CodeCatalystProject extends CodeCatalyst.ProjectSummary { +export interface CodeCatalystProject extends ProjectSummary { readonly type: 'project' readonly name: string readonly org: Pick @@ -172,7 +173,7 @@ export interface CodeCatalystRepo extends ListSourceRepositoriesItem { readonly project: Pick } -export interface CodeCatalystBranch extends CodeCatalyst.ListSourceRepositoryBranchesItem { +export interface CodeCatalystBranch extends ListSourceRepositoryBranchesItem { readonly type: 'branch' readonly name: string readonly repo: Pick @@ -200,7 +201,7 @@ function toBranch( org: string, project: string, repo: string, - branch: CodeCatalyst.ListSourceRepositoryBranchesItem + branch: ListSourceRepositoryBranchesItem ): CodeCatalystBranch { assertHasProps(branch, 'name') @@ -229,10 +230,7 @@ function createCodeCatalystClient( }) } -export type UserDetails = RequiredProps< - CodeCatalyst.GetUserDetailsResponse, - 'userId' | 'userName' | 'displayName' | 'primaryEmail' -> +export type UserDetails = RequiredProps // CodeCatalyst client has two variants: 'logged-in' and 'not logged-in' // The 'not logged-in' variant is a subtype and has restricted functionality @@ -421,7 +419,7 @@ class CodeCatalystClientInternal extends ClientWrapper { } } - public async getSubscription(request: GetSubscriptionRequest): Promise { + public async getSubscription(request: GetSubscriptionRequest): Promise { return this.call(GetSubscriptionCommand, request, false) } @@ -842,18 +840,18 @@ class CodeCatalystClientInternal extends ClientWrapper { startAttempts++ await this.startDevEnvironment(args) } catch (e) { - const err = e as AWS.AWSError + const err = e as ServiceException // - ServiceQuotaExceededException: account billing limit reached // - ValidationException: "… creation has failed, cannot start" // - ConflictException: "Cannot start … because update process is still going on" // (can happen after "Update Dev Environment") - if (err.code === 'ServiceQuotaExceededException') { + if (err.name === 'ServiceQuotaExceededException') { throw new ToolkitError('Dev Environment failed: quota exceeded', { code: 'ServiceQuotaExceeded', cause: err, }) } - doLog('info', `devenv not started (${err.code}), waiting`) + doLog('info', `devenv not started (${err.name}), waiting`) // Continue retrying... } } else if (resp.status === 'STOPPING') { diff --git a/packages/core/src/shared/clients/docdbClient.ts b/packages/core/src/shared/clients/docdbClient.ts index a613071d26e..54050101149 100644 --- a/packages/core/src/shared/clients/docdbClient.ts +++ b/packages/core/src/shared/clients/docdbClient.ts @@ -37,11 +37,16 @@ export class DefaultDocumentDBClient { private async getSdkConfig() { const credentials = await globals.awsContext.getCredentials() - return { + const endpointUrl = globals.awsContext.getCredentialEndpointUrl() + const config = { customUserAgent: getUserAgent({ includePlatform: true, includeClientId: true }), credentials: credentials, region: this.regionCode, } + if (endpointUrl !== undefined) { + return { ...config, endpoint: endpointUrl } + } + return config } public async getClient(): Promise { diff --git a/packages/core/src/shared/clients/ec2MetadataClient.ts b/packages/core/src/shared/clients/ec2MetadataClient.ts index 899adb6761c..72249efa6c9 100644 --- a/packages/core/src/shared/clients/ec2MetadataClient.ts +++ b/packages/core/src/shared/clients/ec2MetadataClient.ts @@ -5,7 +5,8 @@ import { getLogger } from '../logger/logger' import { ClassToInterfaceType } from '../utilities/tsUtils' -import { AWSError, MetadataService } from 'aws-sdk' +import { httpRequest } from '@smithy/credential-provider-imds' +import { RequestOptions } from 'http' export interface IamInfo { Code: string @@ -21,8 +22,12 @@ export interface InstanceIdentity { export type Ec2MetadataClient = ClassToInterfaceType export class DefaultEc2MetadataClient { private static readonly metadataServiceTimeout: number = 500 + // AWS EC2 Instance Metadata Service (IMDS) constants + // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-v2-how-it-works.html + private static readonly metadataServiceHost: string = '169.254.169.254' + private static readonly tokenPath: string = '/latest/api/token' - public constructor(private metadata: MetadataService = DefaultEc2MetadataClient.getMetadataService()) {} + public constructor() {} public getInstanceIdentity(): Promise { return this.invoke('/latest/dynamic/instance-identity/document') @@ -32,52 +37,61 @@ export class DefaultEc2MetadataClient { return this.invoke('/latest/meta-data/iam/info') } - public invoke(path: string): Promise { - return new Promise((resolve, reject) => { - // fetchMetadataToken is private for some reason, but has the exact token functionality - // that we want out of the metadata service. - // https://github.com/aws/aws-sdk-js/blob/3333f8b49283f5bbff823ab8a8469acedb7fe3d5/lib/metadata_service.js#L116-L136 - ;(this.metadata as any).fetchMetadataToken((tokenErr: AWSError, token: string) => { - let options - if (tokenErr) { - getLogger().warn( - 'Ec2MetadataClient failed to fetch token. If this is an EC2 environment, then Toolkit will fall back to IMDSv1: %s', - tokenErr - ) + public async invoke(path: string): Promise { + try { + // Try to get IMDSv2 token first + const token = await this.fetchMetadataToken() + const headers: Record = {} + if (token) { + headers['x-aws-ec2-metadata-token'] = token + } - // Fall back to IMDSv1 for legacy instances. - options = {} - } else { - options = { - // By attaching the token we force the use of IMDSv2. - // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-v2-how-it-works.html - headers: { 'x-aws-ec2-metadata-token': token }, - } - } + const response = await this.makeRequest(path, headers) + return JSON.parse(response.toString()) + } catch (tokenErr) { + getLogger().warn( + 'Ec2MetadataClient failed to fetch token. If this is an EC2 environment, then Toolkit will fall back to IMDSv1: %s', + tokenErr + ) - this.metadata.request(path, options, (err, response) => { - if (err) { - reject(err) - return - } - try { - const jsonResponse: T = JSON.parse(response) - resolve(jsonResponse) - } catch (e) { - reject(`Ec2MetadataClient: invalid response from "${path}": ${response}\nerror: ${e}`) - } - }) - }) - }) + // Fall back to IMDSv1 for legacy instances + try { + const response = await this.makeRequest(path, {}) + return JSON.parse(response.toString()) + } catch (err) { + throw new Error(`Ec2MetadataClient: failed to fetch "${path}": ${err}`) + } + } } - private static getMetadataService() { - return new MetadataService({ - httpOptions: { + private async fetchMetadataToken(): Promise { + try { + const options: RequestOptions = { + host: DefaultEc2MetadataClient.metadataServiceHost, + path: DefaultEc2MetadataClient.tokenPath, + method: 'PUT', + headers: { + 'x-aws-ec2-metadata-token-ttl-seconds': '21600', + }, timeout: DefaultEc2MetadataClient.metadataServiceTimeout, - connectTimeout: DefaultEc2MetadataClient.metadataServiceTimeout, - } as any, - // workaround for known bug: https://github.com/aws/aws-sdk-js/issues/3029 - }) + } + + const response = await httpRequest(options) + return response.toString() + } catch (err) { + return undefined + } + } + + private async makeRequest(path: string, headers: Record): Promise { + const options: RequestOptions = { + host: DefaultEc2MetadataClient.metadataServiceHost, + path, + method: 'GET', + headers, + timeout: DefaultEc2MetadataClient.metadataServiceTimeout, + } + + return httpRequest(options) } } diff --git a/packages/core/src/shared/clients/ecrClient.ts b/packages/core/src/shared/clients/ecrClient.ts index 1478d76751d..f5e03d4db7a 100644 --- a/packages/core/src/shared/clients/ecrClient.ts +++ b/packages/core/src/shared/clients/ecrClient.ts @@ -3,23 +3,31 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ECR } from 'aws-sdk' +import { + ECRClient, + DescribeImagesCommand, + DescribeRepositoriesCommand, + CreateRepositoryCommand, + DeleteRepositoryCommand, + BatchDeleteImageCommand, +} from '@aws-sdk/client-ecr' +import type { DescribeImagesRequest, DescribeRepositoriesRequest, Repository } from '@aws-sdk/client-ecr' import globals from '../extensionGlobals' import { AsyncCollection } from '../utilities/asyncCollection' import { pageableToCollection } from '../utilities/collectionUtils' import { assertHasProps, ClassToInterfaceType, isNonNullable, RequiredProps } from '../utilities/tsUtils' -export type EcrRepository = RequiredProps +export type EcrRepository = RequiredProps export type EcrClient = ClassToInterfaceType export class DefaultEcrClient { public constructor(public readonly regionCode: string) {} public async *describeTags(repositoryName: string): AsyncIterableIterator { - const sdkClient = await this.createSdkClient() - const request: ECR.DescribeImagesRequest = { repositoryName: repositoryName } + const sdkClient = this.createSdkClient() + const request: DescribeImagesRequest = { repositoryName: repositoryName } do { - const response = await sdkClient.describeImages(request).promise() + const response = await sdkClient.send(new DescribeImagesCommand(request)) if (response.imageDetails) { for (const item of response.imageDetails) { if (item.imageTags !== undefined) { @@ -34,13 +42,13 @@ export class DefaultEcrClient { } public async *describeRepositories(): AsyncIterableIterator { - const sdkClient = await this.createSdkClient() - const request: ECR.DescribeRepositoriesRequest = {} + const sdkClient = this.createSdkClient() + const request: DescribeRepositoriesRequest = {} do { - const response = await sdkClient.describeRepositories(request).promise() + const response = await sdkClient.send(new DescribeRepositoriesCommand(request)) if (response.repositories) { yield* response.repositories - .map((repo) => { + .map((repo: Repository) => { // If any of these are not present, the repo returned is not valid. repositoryUri/Arn // are both based on name, and it's not possible to not have a name if (!repo.repositoryArn || !repo.repositoryName || !repo.repositoryUri) { @@ -53,36 +61,43 @@ export class DefaultEcrClient { } } }) - .filter((item) => item !== undefined) as EcrRepository[] + .filter((item: EcrRepository | undefined) => item !== undefined) as EcrRepository[] } request.nextToken = response.nextToken } while (request.nextToken) } public listAllRepositories(): AsyncCollection { - const requester = async (req: ECR.DescribeRepositoriesRequest) => - (await this.createSdkClient()).describeRepositories(req).promise() + const requester = async (req: DescribeRepositoriesRequest) => + this.createSdkClient().send(new DescribeRepositoriesCommand(req)) const collection = pageableToCollection(requester, {}, 'nextToken', 'repositories') - return collection.filter(isNonNullable).map((list) => list.map((repo) => (assertHasProps(repo), repo))) + return collection + .filter(isNonNullable) + .map((list: Repository[]) => list.map((repo: Repository) => (assertHasProps(repo), repo))) } public async createRepository(repositoryName: string) { - const sdkClient = await this.createSdkClient() - return sdkClient.createRepository({ repositoryName: repositoryName }).promise() + const sdkClient = this.createSdkClient() + return sdkClient.send(new CreateRepositoryCommand({ repositoryName: repositoryName })) } public async deleteRepository(repositoryName: string): Promise { - const sdkClient = await this.createSdkClient() - await sdkClient.deleteRepository({ repositoryName: repositoryName }).promise() + const sdkClient = this.createSdkClient() + await sdkClient.send(new DeleteRepositoryCommand({ repositoryName: repositoryName })) } public async deleteTag(repositoryName: string, tag: string): Promise { - const sdkClient = await this.createSdkClient() - await sdkClient.batchDeleteImage({ repositoryName: repositoryName, imageIds: [{ imageTag: tag }] }).promise() + const sdkClient = this.createSdkClient() + await sdkClient.send( + new BatchDeleteImageCommand({ repositoryName: repositoryName, imageIds: [{ imageTag: tag }] }) + ) } - protected async createSdkClient(): Promise { - return await globals.sdkClientBuilder.createAwsService(ECR, undefined, this.regionCode) + protected createSdkClient(): ECRClient { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: ECRClient, + clientOptions: { region: this.regionCode }, + }) } } diff --git a/packages/core/src/shared/clients/ecsClient.ts b/packages/core/src/shared/clients/ecsClient.ts index 51bda018502..818cdc0dec5 100644 --- a/packages/core/src/shared/clients/ecsClient.ts +++ b/packages/core/src/shared/clients/ecsClient.ts @@ -3,7 +3,31 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ECS } from 'aws-sdk' +import { + Cluster, + DescribeClustersCommand, + DescribeServicesCommand, + DescribeTaskDefinitionCommand, + DescribeTaskDefinitionResponse, + DescribeTasksCommand, + DescribeTasksRequest, + ECSClient, + ExecuteCommandCommand, + ExecuteCommandRequest, + ExecuteCommandResponse, + ListClustersCommand, + ListClustersRequest, + ListServicesCommand, + ListServicesRequest, + ListTasksCommand, + ListTasksRequest, + RegisterTaskDefinitionCommand, + RegisterTaskDefinitionRequest, + Service, + Task, + UpdateServiceCommand, + UpdateServiceRequest, +} from '@aws-sdk/client-ecs' import globals from '../extensionGlobals' import { AsyncCollection } from '../utilities/asyncCollection' import { pageableToCollection } from '../utilities/collectionUtils' @@ -12,7 +36,7 @@ import { ClassToInterfaceType, isNonNullable } from '../utilities/tsUtils' export type EcsClient = ClassToInterfaceType export type EcsResourceAndToken = { - resource: ECS.Cluster[] | ECS.Service[] + resource: Cluster[] | Service[] nextToken?: string } @@ -21,12 +45,16 @@ export class DefaultEcsClient { public constructor(public readonly regionCode: string) {} public async getClusters(nextToken?: string): Promise { - const sdkClient = await this.createSdkClient() - const clusterArnList = await sdkClient.listClusters({ maxResults: maxResultsPerResponse, nextToken }).promise() + const sdkClient = this.createSdkClient() + const clusterArnList = await sdkClient.send( + new ListClustersCommand({ maxResults: maxResultsPerResponse, nextToken }) + ) if (clusterArnList.clusterArns?.length === 0) { return { resource: [] } } - const clusterResponse = await sdkClient.describeClusters({ clusters: clusterArnList.clusterArns }).promise() + const clusterResponse = await sdkClient.send( + new DescribeClustersCommand({ clusters: clusterArnList.clusterArns }) + ) const response: EcsResourceAndToken = { resource: clusterResponse.clusters!, nextToken: clusterArnList.nextToken, @@ -34,9 +62,9 @@ export class DefaultEcsClient { return response } - public listClusters(request: ECS.ListClustersRequest = {}): AsyncCollection { + public listClusters(request: ListClustersRequest = {}): AsyncCollection { const client = this.createSdkClient() - const requester = async (req: ECS.ListClustersRequest) => (await client).listClusters(req).promise() + const requester = async (req: ListClustersRequest) => client.send(new ListClustersCommand(req)) const collection = pageableToCollection(requester, request, 'nextToken', 'clusterArns') return collection.filter(isNonNullable).map(async (clusters) => { @@ -44,16 +72,16 @@ export class DefaultEcsClient { return [] } - const resp = await (await client).describeClusters({ clusters }).promise() + const resp = await client.send(new DescribeClustersCommand({ clusters })) return resp.clusters! }) } public async getServices(cluster: string, nextToken?: string): Promise { - const sdkClient = await this.createSdkClient() - const serviceArnList = await sdkClient - .listServices({ cluster: cluster, maxResults: maxResultsPerResponse, nextToken }) - .promise() + const sdkClient = this.createSdkClient() + const serviceArnList = await sdkClient.send( + new ListServicesCommand({ cluster: cluster, maxResults: maxResultsPerResponse, nextToken }) + ) if (serviceArnList.serviceArns?.length === 0) { return { resource: [] } } @@ -65,9 +93,9 @@ export class DefaultEcsClient { return response } - public listServices(request: ECS.ListServicesRequest = {}): AsyncCollection { + public listServices(request: ListServicesRequest = {}): AsyncCollection { const client = this.createSdkClient() - const requester = async (req: ECS.ListServicesRequest) => (await client).listServices(req).promise() + const requester = async (req: ListServicesRequest) => client.send(new ListServicesCommand(req)) const collection = pageableToCollection(requester, request, 'nextToken', 'serviceArns') return collection.filter(isNonNullable).map(async (services) => { @@ -75,56 +103,57 @@ export class DefaultEcsClient { return [] } - const resp = await (await client).describeServices({ cluster: request.cluster, services }).promise() + const resp = await client.send(new DescribeServicesCommand({ cluster: request.cluster, services })) return resp.services! }) } - public async describeTaskDefinition(taskDefinition: string): Promise { - const sdkClient = await this.createSdkClient() - return await sdkClient.describeTaskDefinition({ taskDefinition }).promise() + public async describeTaskDefinition(taskDefinition: string): Promise { + const sdkClient = this.createSdkClient() + return await sdkClient.send(new DescribeTaskDefinitionCommand({ taskDefinition })) } - public async listTasks(args: ECS.ListTasksRequest): Promise { - const sdkClient = await this.createSdkClient() - const listTasksResponse = await sdkClient.listTasks(args).promise() + public async listTasks(args: ListTasksRequest): Promise { + const sdkClient = this.createSdkClient() + const listTasksResponse = await sdkClient.send(new ListTasksCommand(args)) return listTasksResponse.taskArns ?? [] } - public async updateService(request: ECS.UpdateServiceRequest): Promise { - const sdkClient = await this.createSdkClient() - await sdkClient.updateService(request).promise() + public async updateService(request: UpdateServiceRequest): Promise { + const sdkClient = this.createSdkClient() + await sdkClient.send(new UpdateServiceCommand(request)) } - public async describeTasks(cluster: string, tasks: string[]): Promise { - const sdkClient = await this.createSdkClient() + public async describeTasks(cluster: string, tasks: string[]): Promise { + const sdkClient = this.createSdkClient() - const params: ECS.DescribeTasksRequest = { cluster, tasks } - const describedTasks = await sdkClient.describeTasks(params).promise() + const params: DescribeTasksRequest = { cluster, tasks } + const describedTasks = await sdkClient.send(new DescribeTasksCommand(params)) return describedTasks.tasks ?? [] } - public async describeServices(cluster: string, services: string[]): Promise { - const sdkClient = await this.createSdkClient() - return (await sdkClient.describeServices({ cluster, services }).promise()).services ?? [] + public async describeServices(cluster: string, services: string[]): Promise { + const sdkClient = this.createSdkClient() + return (await sdkClient.send(new DescribeServicesCommand({ cluster, services }))).services ?? [] } - protected async createSdkClient(): Promise { - return await globals.sdkClientBuilder.createAwsService(ECS, undefined, this.regionCode) + protected createSdkClient(): ECSClient { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: ECSClient, + clientOptions: { region: this.regionCode }, + }) } - public async executeCommand( - request: Omit - ): Promise { - const sdkClient = await this.createSdkClient() + public async executeCommand(request: Omit): Promise { + const sdkClient = this.createSdkClient() // Currently the 'interactive' flag is required and needs to be true for ExecuteCommand: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ExecuteCommand.html // This may change 'in the near future' as explained here: https://aws.amazon.com/blogs/containers/new-using-amazon-ecs-exec-access-your-containers-fargate-ec2/ - return await sdkClient.executeCommand({ ...request, interactive: true }).promise() + return await sdkClient.send(new ExecuteCommandCommand({ ...request, interactive: true })) } - public async registerTaskDefinition(request: ECS.RegisterTaskDefinitionRequest) { - const sdkClient = await this.createSdkClient() - return sdkClient.registerTaskDefinition(request).promise() + public async registerTaskDefinition(request: RegisterTaskDefinitionRequest) { + const sdkClient = this.createSdkClient() + return sdkClient.send(new RegisterTaskDefinitionCommand(request)) } } diff --git a/packages/core/src/shared/clients/iotClient.ts b/packages/core/src/shared/clients/iotClient.ts index 45b9cbd4e4f..fc5581fffa4 100644 --- a/packages/core/src/shared/clients/iotClient.ts +++ b/packages/core/src/shared/clients/iotClient.ts @@ -4,7 +4,70 @@ */ import * as _ from 'lodash' -import { Iot } from 'aws-sdk' +import { + AttachPolicyCommand, + AttachPolicyRequest, + AttachThingPrincipalCommand, + AttachThingPrincipalRequest, + CertificateDescription, + CreateKeysAndCertificateCommand, + CreateKeysAndCertificateRequest, + CreateKeysAndCertificateResponse, + CreatePolicyCommand, + CreatePolicyRequest, + CreatePolicyResponse, + CreatePolicyVersionCommand, + CreatePolicyVersionRequest, + CreateThingCommand, + CreateThingRequest, + CreateThingResponse, + DeleteCertificateCommand, + DeleteCertificateRequest, + DeletePolicyCommand, + DeletePolicyRequest, + DeletePolicyVersionCommand, + DeletePolicyVersionRequest, + DeleteThingCommand, + DeleteThingRequest, + DescribeCertificateCommand, + DescribeCertificateRequest, + DescribeCertificateResponse, + DescribeEndpointCommand, + DetachPolicyCommand, + DetachPolicyRequest, + DetachThingPrincipalCommand, + DetachThingPrincipalRequest, + GetPolicyVersionCommand, + GetPolicyVersionRequest, + GetPolicyVersionResponse, + IoTClient, + ListCertificatesCommand, + ListCertificatesRequest, + ListCertificatesResponse, + ListPoliciesCommand, + ListPoliciesRequest, + ListPoliciesResponse, + ListPolicyVersionsCommand, + ListPolicyVersionsRequest, + ListPrincipalPoliciesCommand, + ListPrincipalPoliciesRequest, + ListPrincipalPoliciesResponse, + ListPrincipalThingsCommand, + ListPrincipalThingsRequest, + ListTargetsForPolicyCommand, + ListTargetsForPolicyRequest, + ListThingPrincipalsCommand, + ListThingPrincipalsRequest, + ListThingPrincipalsResponse, + ListThingsCommand, + ListThingsRequest, + ListThingsResponse, + PolicyVersion, + SetDefaultPolicyVersionCommand, + SetDefaultPolicyVersionRequest, + UpdateCertificateCommand, + UpdateCertificateRequest, +} from '@aws-sdk/client-iot' import { parse } from '@aws-sdk/util-arn-parser' import { getLogger } from '../logger/logger' import { InterfaceNoSymbol } from '../utilities/tsUtils' @@ -30,14 +93,14 @@ const iotServiceArn = 'iot' const certArnResourcePattern = /cert\/(\w+)/ export interface ListThingCertificatesResponse { - readonly certificates: Iot.CertificateDescription[] + readonly certificates: CertificateDescription[] readonly nextToken: string | undefined } export class DefaultIotClient { public constructor( private readonly regionCode: string, - private readonly iotProvider: (regionCode: string) => Promise = createSdkClient + private readonly iotProvider: (regionCode: string) => IoTClient = createSdkClient ) {} /** @@ -45,16 +108,16 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async listThings(request?: Iot.ListThingsRequest): Promise { + public async listThings(request?: ListThingsRequest): Promise { getLogger().debug('ListThings called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output: Iot.ListThingsResponse = await iot - .listThings({ + const output: ListThingsResponse = await iot.send( + new ListThingsCommand({ maxResults: request?.maxResults ?? defaultMaxThings, nextToken: request?.nextToken, }) - .promise() + ) getLogger().debug('ListThings returned response: %O', output) return output @@ -65,11 +128,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async createThing(request: Iot.CreateThingRequest): Promise { + public async createThing(request: CreateThingRequest): Promise { getLogger().debug('CreateThing called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output: Iot.CreateThingResponse = await iot.createThing({ thingName: request.thingName }).promise() + const output: CreateThingResponse = await iot.send(new CreateThingCommand({ thingName: request.thingName })) getLogger().debug('CreateThing returned response: %O', output) return output @@ -80,11 +143,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async deleteThing(request: Iot.DeleteThingRequest): Promise { + public async deleteThing(request: DeleteThingRequest): Promise { getLogger().debug('DeleteThing called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - await iot.deleteThing({ thingName: request.thingName }).promise() + await iot.send(new DeleteThingCommand({ thingName: request.thingName })) getLogger().debug('DeleteThing successful') } @@ -94,17 +157,17 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async listCertificates(request: Iot.ListCertificatesRequest): Promise { + public async listCertificates(request: ListCertificatesRequest): Promise { getLogger().debug('ListCertificates called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output: Iot.ListCertificatesResponse = await iot - .listCertificates({ + const output: ListCertificatesResponse = await iot.send( + new ListCertificatesCommand({ pageSize: request.pageSize ?? defaultMaxThings, marker: request.marker, ascendingOrder: request.ascendingOrder, }) - .promise() + ) getLogger().debug('ListCertificates returned response: %O', output) return output @@ -118,18 +181,16 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async listThingPrincipals( - request: Iot.ListThingPrincipalsRequest - ): Promise { - const iot = await this.iotProvider(this.regionCode) + public async listThingPrincipals(request: ListThingPrincipalsRequest): Promise { + const iot = this.iotProvider(this.regionCode) - const output: Iot.ListThingPrincipalsResponse = await iot - .listThingPrincipals({ + const output: ListThingPrincipalsResponse = await iot.send( + new ListThingPrincipalsCommand({ thingName: request.thingName, maxResults: request.maxResults ?? defaultMaxThings, nextToken: request.nextToken, }) - .promise() + ) return output } @@ -138,12 +199,10 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - private async describeCertificate( - request: Iot.DescribeCertificateRequest - ): Promise { - const iot = await this.iotProvider(this.regionCode) + private async describeCertificate(request: DescribeCertificateRequest): Promise { + const iot = this.iotProvider(this.regionCode) - const output: Iot.DescribeCertificateResponse = await iot.describeCertificate(request).promise() + const output: DescribeCertificateResponse = await iot.send(new DescribeCertificateCommand(request)) return output } @@ -158,13 +217,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async listThingCertificates( - request: Iot.ListThingPrincipalsRequest - ): Promise { + public async listThingCertificates(request: ListThingPrincipalsRequest): Promise { getLogger().debug('ListThingCertificates called with request: %O', request) const output = await this.listThingPrincipals(request) - const iotPrincipals: Iot.Principal[] = output.principals ?? [] + const iotPrincipals: string[] = output.principals ?? [] const nextToken = output.nextToken const describedCerts = iotPrincipals.map(async (iotPrincipal) => { @@ -179,7 +236,7 @@ export class DefaultIotClient { const resolvedCerts = (await Promise.all(describedCerts)) .filter((cert) => cert?.certificateDescription !== undefined) - .map((cert) => cert?.certificateDescription as Iot.CertificateDescription) + .map((cert) => cert?.certificateDescription as CertificateDescription) const response: ListThingCertificatesResponse = { certificates: resolvedCerts, nextToken: nextToken } getLogger().debug('ListThingCertificates returned response: %O', response) @@ -194,18 +251,18 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async listThingsForCert(request: Iot.ListPrincipalThingsRequest): Promise { + public async listThingsForCert(request: ListPrincipalThingsRequest): Promise { getLogger().debug('ListThingsForCert called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output = await iot - .listPrincipalThings({ + const output = await iot.send( + new ListPrincipalThingsCommand({ maxResults: request.maxResults ?? defaultMaxThings, nextToken: request.nextToken, principal: request.principal, }) - .promise() - const iotThings: Iot.ThingName[] = output.things ?? [] + ) + const iotThings: string[] = output.things ?? [] getLogger().debug('ListThingsForCert returned response: %O', iotThings) return iotThings @@ -217,12 +274,12 @@ export class DefaultIotClient { * @throws Error if there is an error calling IoT. */ public async createCertificateAndKeys( - request: Iot.CreateKeysAndCertificateRequest - ): Promise { + request: CreateKeysAndCertificateRequest + ): Promise { getLogger().debug('CreateCertificate called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output: Iot.CreateKeysAndCertificateResponse = await iot.createKeysAndCertificate(request).promise() + const output: CreateKeysAndCertificateResponse = await iot.send(new CreateKeysAndCertificateCommand(request)) getLogger().debug('CreateCertificate succeeded') return output @@ -233,11 +290,13 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async updateCertificate(request: Iot.UpdateCertificateRequest): Promise { + public async updateCertificate(request: UpdateCertificateRequest): Promise { getLogger().debug('UpdateCertificate called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - await iot.updateCertificate({ certificateId: request.certificateId, newStatus: request.newStatus }).promise() + await iot.send( + new UpdateCertificateCommand({ certificateId: request.certificateId, newStatus: request.newStatus }) + ) getLogger().debug('UpdateCertificate successful') } @@ -251,11 +310,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async deleteCertificate(request: Iot.DeleteCertificateRequest): Promise { + public async deleteCertificate(request: DeleteCertificateRequest): Promise { getLogger().debug('DeleteCertificate called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - await iot.deleteCertificate(request).promise() + await iot.send(new DeleteCertificateCommand(request)) getLogger().debug('DeleteCertificate successful') } @@ -265,11 +324,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async attachThingPrincipal(request: Iot.AttachThingPrincipalRequest): Promise { + public async attachThingPrincipal(request: AttachThingPrincipalRequest): Promise { getLogger().debug('AttachThingPrincipal called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - await iot.attachThingPrincipal({ thingName: request.thingName, principal: request.principal }).promise() + await iot.send(new AttachThingPrincipalCommand({ thingName: request.thingName, principal: request.principal })) getLogger().debug('AttachThingPrincipal successful') } @@ -279,11 +338,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async detachThingPrincipal(request: Iot.DetachThingPrincipalRequest): Promise { + public async detachThingPrincipal(request: DetachThingPrincipalRequest): Promise { getLogger().debug('DetachThingPrincipal called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - await iot.detachThingPrincipal({ thingName: request.thingName, principal: request.principal }).promise() + await iot.send(new DetachThingPrincipalCommand({ thingName: request.thingName, principal: request.principal })) getLogger().debug('DetachThingPrincipal successful') } @@ -293,17 +352,17 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async listPolicies(request: Iot.ListPoliciesRequest): Promise { + public async listPolicies(request: ListPoliciesRequest): Promise { getLogger().debug('ListPolicies called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output: Iot.ListPoliciesResponse = await iot - .listPolicies({ + const output: ListPoliciesResponse = await iot.send( + new ListPoliciesCommand({ pageSize: request.pageSize ?? defaultMaxThings, marker: request.marker, ascendingOrder: request.ascendingOrder, }) - .promise() + ) getLogger().debug('ListPolicies returned response: %O', output) return output @@ -314,18 +373,18 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async listPrincipalPolicies(request: Iot.ListPrincipalPoliciesRequest): Promise { + public async listPrincipalPolicies(request: ListPrincipalPoliciesRequest): Promise { getLogger().debug('ListPrincipalPolicies called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output: Iot.ListPrincipalPoliciesResponse = await iot - .listPrincipalPolicies({ + const output: ListPrincipalPoliciesResponse = await iot.send( + new ListPrincipalPoliciesCommand({ principal: request.principal, pageSize: request.pageSize ?? defaultMaxThings, marker: request.marker, ascendingOrder: request.ascendingOrder, }) - .promise() + ) getLogger().debug('ListPrincipalPolicies returned response: %O', output) return output @@ -339,18 +398,18 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async listPolicyTargets(request: Iot.ListTargetsForPolicyRequest): Promise { + public async listPolicyTargets(request: ListTargetsForPolicyRequest): Promise { getLogger().debug('ListPolicyTargets called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output = await iot - .listTargetsForPolicy({ + const output = await iot.send( + new ListTargetsForPolicyCommand({ pageSize: request.pageSize ?? defaultMaxThings, marker: request.marker, policyName: request.policyName, }) - .promise() - const arns: Iot.Target[] = output.targets ?? [] + ) + const arns: string[] = output.targets ?? [] getLogger().debug('ListPolicyTargets returned response: %O', arns) return arns @@ -361,11 +420,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async attachPolicy(request: Iot.AttachPolicyRequest): Promise { + public async attachPolicy(request: AttachPolicyRequest): Promise { getLogger().debug('AttachPolicy called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - await iot.attachPolicy({ policyName: request.policyName, target: request.target }).promise() + await iot.send(new AttachPolicyCommand({ policyName: request.policyName, target: request.target })) getLogger().debug('AttachPolicy successful') } @@ -375,11 +434,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async detachPolicy(request: Iot.DetachPolicyRequest): Promise { + public async detachPolicy(request: DetachPolicyRequest): Promise { getLogger().debug('DetachPolicy called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - await iot.detachPolicy({ policyName: request.policyName, target: request.target }).promise() + await iot.send(new DetachPolicyCommand({ policyName: request.policyName, target: request.target })) getLogger().debug('DetachPolicy successful') } @@ -389,11 +448,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async createPolicy(request: Iot.CreatePolicyRequest): Promise { + public async createPolicy(request: CreatePolicyRequest): Promise { getLogger().debug('CreatePolicy called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output: Iot.CreatePolicyResponse = await iot.createPolicy(request).promise() + const output: CreatePolicyResponse = await iot.send(new CreatePolicyCommand(request)) getLogger().info(`Created policy: ${output.policyArn}`) getLogger().debug('CreatePolicy successful') @@ -408,11 +467,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async deletePolicy(request: Iot.DeletePolicyRequest): Promise { + public async deletePolicy(request: DeletePolicyRequest): Promise { getLogger().debug('DeletePolicy called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - await iot.deletePolicy({ policyName: request.policyName }).promise() + await iot.send(new DeletePolicyCommand({ policyName: request.policyName })) getLogger().debug('DeletePolicy successful') } @@ -424,9 +483,9 @@ export class DefaultIotClient { */ public async getEndpoint(): Promise { getLogger().debug('GetEndpoint called') - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output = await iot.describeEndpoint({ endpointType: iotEndpointType }).promise() + const output = await iot.send(new DescribeEndpointCommand({ endpointType: iotEndpointType })) if (!output.endpointAddress) { throw new Error('Failed to retrieve endpoint') } @@ -440,10 +499,10 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async *listPolicyVersions(request: Iot.ListPolicyVersionsRequest): AsyncIterableIterator { - const iot = await this.iotProvider(this.regionCode) + public async *listPolicyVersions(request: ListPolicyVersionsRequest): AsyncIterableIterator { + const iot = this.iotProvider(this.regionCode) - const response = await iot.listPolicyVersions(request).promise() + const response = await iot.send(new ListPolicyVersionsCommand(request)) if (response.policyVersions) { yield* response.policyVersions @@ -455,11 +514,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async createPolicyVersion(request: Iot.CreatePolicyVersionRequest): Promise { + public async createPolicyVersion(request: CreatePolicyVersionRequest): Promise { getLogger().debug('CreatePolicyVersion called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output = await iot.createPolicyVersion(request).promise() + const output = await iot.send(new CreatePolicyVersionCommand(request)) getLogger().info(`Created new version ${output.policyVersionId} of ${request.policyName}`) getLogger().debug('CreatePolicyVersion successful') @@ -474,11 +533,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async deletePolicyVersion(request: Iot.DeletePolicyVersionRequest): Promise { + public async deletePolicyVersion(request: DeletePolicyVersionRequest): Promise { getLogger().debug('DeletePolicyVersion called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - await iot.deletePolicyVersion(request).promise() + await iot.send(new DeletePolicyVersionCommand(request)) getLogger().debug('DeletePolicyVersion successful') } @@ -488,11 +547,11 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async setDefaultPolicyVersion(request: Iot.SetDefaultPolicyVersionRequest): Promise { + public async setDefaultPolicyVersion(request: SetDefaultPolicyVersionRequest): Promise { getLogger().debug('SetDefaultPolicyVersion called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - await iot.setDefaultPolicyVersion(request).promise() + await iot.send(new SetDefaultPolicyVersionCommand(request)) getLogger().debug('SetDefaultPolicyVersion successful') } @@ -502,17 +561,20 @@ export class DefaultIotClient { * * @throws Error if there is an error calling IoT. */ - public async getPolicyVersion(request: Iot.GetPolicyVersionRequest): Promise { + public async getPolicyVersion(request: GetPolicyVersionRequest): Promise { getLogger().debug('GetPolicyVersion called with request: %O', request) - const iot = await this.iotProvider(this.regionCode) + const iot = this.iotProvider(this.regionCode) - const output: Iot.GetPolicyVersionResponse = await iot.getPolicyVersion(request).promise() + const output: GetPolicyVersionResponse = await iot.send(new GetPolicyVersionCommand(request)) getLogger().debug('GetPolicyVersion successful') return output } } -async function createSdkClient(regionCode: string): Promise { - return await globals.sdkClientBuilder.createAwsService(Iot, undefined, regionCode) +function createSdkClient(regionCode: string): IoTClient { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: IoTClient, + clientOptions: { region: regionCode }, + }) } diff --git a/packages/core/src/shared/clients/kubectlClient.ts b/packages/core/src/shared/clients/kubectlClient.ts new file mode 100644 index 00000000000..a374927bda4 --- /dev/null +++ b/packages/core/src/shared/clients/kubectlClient.ts @@ -0,0 +1,270 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as k8s from '@kubernetes/client-node' +import { getLogger } from '../logger/logger' +import { Cluster } from '@aws-sdk/client-eks' +import { SagemakerDevSpaceNode } from '../../awsService/sagemaker/explorer/sagemakerDevSpaceNode' +import globals from '../extensionGlobals' + +export interface HyperpodDevSpace { + name: string + namespace: string + cluster: string + group: string + version: string + plural: string + status: string + appType: string + creator: string + accessType: string +} + +export interface HyperpodCluster { + clusterName: string + clusterArn: string + status: string + eksClusterName?: string + eksClusterArn?: string + regionCode: string +} + +export class KubectlClient { + private kubeConfig: k8s.KubeConfig + private k8sApi: k8s.CustomObjectsApi + + public constructor(eksCluster: Cluster, hyperpodCluster: HyperpodCluster) { + this.kubeConfig = new k8s.KubeConfig() + this.loadKubeConfig(eksCluster, hyperpodCluster) + this.k8sApi = this.kubeConfig.makeApiClient(k8s.CustomObjectsApi) + } + + async getSpacesForCluster(eksCluster: Cluster): Promise { + try { + const group = 'workspace.jupyter.org' + const version = 'v1alpha1' + const plural = 'workspaces' + + const res = await this.k8sApi!.listClusterCustomObject(group, version, plural) + if (!res) { + getLogger().info(`No cluster custom object found`) + return [] + } + + if ((res as any).body?.items) { + return (res as any).body.items.map((space: any) => ({ + name: space.metadata?.name, + namespace: space.metadata?.namespace, + cluster: eksCluster.name, + status: this.getStatusFromConditions(space.status?.conditions, space.spec?.desiredStatus), + group, + version, + plural, + appType: space.spec?.appType, + creator: space.metadata?.annotations['workspace.jupyter.org/created-by'], + accessType: space.spec?.accessType, + })) + } + } catch (error: any) { + if (error.statusCode === 403 || error.statusCode === 401) { + void vscode.window.showErrorMessage( + `You do not have permission to view ${eksCluster.name} or its spaces. Please contact your administrator.` + ) + getLogger().warn( + `[Warning]: User has insufficient permissions to view EKS cluster (${eksCluster.name}) or its spaces.` + ) + } + + getLogger().warn( + `[Warning]: Unavailable spaces for EKS Cluster (${eksCluster.name}): ${error}\nStack trace: ${(error as Error).stack}` + ) + } + return [] + } + + private loadKubeConfig(eksCluster: Cluster, hyperpodCluster: HyperpodCluster): void { + if (eksCluster.name && eksCluster.endpoint) { + const credentialId = globals.awsContext.getCredentialProfileName() + const awsProfile = credentialId?.startsWith('profile:') ? credentialId.split('profile:')[1] : credentialId + this.kubeConfig.loadFromOptions({ + clusters: [ + { + name: eksCluster.name, + server: eksCluster.endpoint, + caData: eksCluster.certificateAuthority?.data, + skipTLSVerify: false, + }, + ], + users: [ + { + name: eksCluster.name, + exec: { + apiVersion: 'client.authentication.k8s.io/v1beta1', + command: 'aws', + args: [ + 'eks', + 'get-token', + '--cluster-name', + eksCluster.name, + '--region', + hyperpodCluster.regionCode, + ], + env: [ + { + name: 'AWS_PROFILE', + value: awsProfile, + }, + ], + interactiveMode: 'Never', + }, + }, + ], + contexts: [ + { + name: eksCluster.name, + cluster: eksCluster.name, + user: eksCluster.name, + }, + ], + currentContext: eksCluster.name, + }) + } + } + + async getHyperpodSpaceStatus(devSpace: HyperpodDevSpace): Promise { + try { + const response = await this.k8sApi!.getNamespacedCustomObject( + devSpace.group, + devSpace.version, + devSpace.namespace, + devSpace.plural, + devSpace.name + ) + + const statusObj = (response.body as any).status + const desiredStatus = (response.body as any).spec?.desiredStatus + const conditions = statusObj?.conditions + const currentStatus = this.getStatusFromConditions(conditions, desiredStatus) + + return currentStatus + } catch (error) { + throw new Error(`[Hyperpod] Failed to get status for devSpace: ${devSpace.name}`) + } + } + + private getStatusFromConditions(conditions: any[], desiredStatus?: string): string { + if (!conditions) { + return 'Unknown' + } + const getCondition = (type: string) => conditions.find((c: any) => c.type === type)?.status === 'True' + + const available = getCondition('Available') + const progressing = getCondition('Progressing') + const stopped = getCondition('Stopped') + const degraded = getCondition('Degraded') + + if (degraded) { + return 'Error' + } else if (!available && progressing && desiredStatus === 'Running') { + return 'Starting' + } else if (!available && progressing && desiredStatus === 'Stopped') { + return 'Stopping' + } else if (available && !progressing && !stopped) { + return 'Running' + } else if (!available && !progressing && stopped) { + return 'Stopped' + } else { + return 'Unknown' + } + } + + async startHyperpodDevSpace(node: SagemakerDevSpaceNode): Promise { + getLogger().info(`[Hyperpod] Starting devSpace: %s`, node.devSpace.name) + await this.patchDevSpaceStatus(node.devSpace, 'Running') + node.devSpace.status = await this.getHyperpodSpaceStatus(node.devSpace) + node.getParent().trackPendingNode(node.getDevSpaceKey()) + } + + async stopHyperpodDevSpace(node: SagemakerDevSpaceNode): Promise { + getLogger().info(`[Hyperpod] Stopping devSpace: %s`, node.devSpace.name) + await this.patchDevSpaceStatus(node.devSpace, 'Stopped') + node.devSpace.status = await this.getHyperpodSpaceStatus(node.devSpace) + node.getParent().trackPendingNode(node.getDevSpaceKey()) + } + + async patchDevSpaceStatus(devSpace: HyperpodDevSpace, desiredStatus: 'Running' | 'Stopped'): Promise { + try { + const patchBody = { + spec: { + desiredStatus: desiredStatus, + }, + } + + await this.k8sApi!.patchNamespacedCustomObject( + devSpace.group, + devSpace.version, + devSpace.namespace, + devSpace.plural, + devSpace.name, + patchBody, + undefined, + undefined, + undefined, + { headers: { 'Content-Type': 'application/merge-patch+json' } } + ) + } catch (error) { + throw new Error( + `[Hyperpod] Failed to update transitional status for devSpace ${devSpace.name}: ${(error as Error).message}` + ) + } + } + + async createWorkspaceConnection(devSpace: HyperpodDevSpace): Promise<{ type: string; url: string }> { + try { + getLogger().info(`[Hyperpod] Creating workspace connection for space: ${devSpace.name}`) + + const group = 'connection.workspace.jupyter.org' + const version = 'v1alpha1' + const plural = 'workspaceconnections' + + const workspaceConnection = { + apiVersion: `${group}/${version}`, + kind: 'WorkspaceConnection', + metadata: { + namespace: devSpace.namespace, + }, + spec: { + workspaceName: devSpace.name, + workspaceConnectionType: 'vscode-remote', + }, + } + + getLogger().info(`[Hyperpod] Creating WorkspaceConnection: %O`, workspaceConnection) + + const response = await this.k8sApi!.createNamespacedCustomObject( + group, + version, + devSpace.namespace, + plural, + workspaceConnection + ) + + const body = response.body as any + const presignedUrl = body.status?.workspaceConnectionUrl + const connectionType = body.status?.workspaceConnectionType + + getLogger().info(`Connection Type: ${connectionType}`) + getLogger().info(`Presigned URL: ${presignedUrl}`) + + return { type: connectionType || 'vscode-remote', url: presignedUrl } + } catch (error) { + getLogger().error(`[Hyperpod] Failed to create workspace connection: ${error}`) + throw new Error( + `Failed to create workspace connection: ${error instanceof Error ? error.message : String(error)}` + ) + } + } +} diff --git a/packages/core/src/shared/clients/lambdaClient.ts b/packages/core/src/shared/clients/lambdaClient.ts index 137af843e65..4872b371f5c 100644 --- a/packages/core/src/shared/clients/lambdaClient.ts +++ b/packages/core/src/shared/clients/lambdaClient.ts @@ -3,17 +3,45 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Lambda } from 'aws-sdk' -import { _Blob } from 'aws-sdk/clients/lambda' +import { BlobPayloadInputTypes } from '@smithy/types' import { ToolkitError } from '../errors' import globals from '../extensionGlobals' import { getLogger } from '../logger/logger' import { ClassToInterfaceType } from '../utilities/tsUtils' -import { LambdaClient as LambdaSdkClient, GetFunctionCommand, GetFunctionCommandOutput } from '@aws-sdk/client-lambda' +import { + LambdaClient as LambdaSdkClient, + GetFunctionCommand, + GetFunctionCommandOutput, + FunctionConfiguration, + InvocationResponse, + ListFunctionsRequest, + ListFunctionsResponse, + GetFunctionResponse, + GetLayerVersionResponse, + ListLayerVersionsRequest, + LayerVersionsListItem, + ListLayerVersionsResponse, + UpdateFunctionConfigurationRequest, + FunctionUrlConfig, + GetFunctionConfigurationCommand, + PublishVersionCommand, + UpdateFunctionConfigurationCommand, + UpdateFunctionCodeCommand, + ListFunctionUrlConfigsCommand, + ListLayerVersionsCommand, + GetLayerVersionCommand, + ListFunctionsCommand, + DeleteFunctionCommand, + InvokeCommand, + waitUntilFunctionUpdatedV2, + waitUntilFunctionActiveV2, +} from '@aws-sdk/client-lambda' import { CancellationError } from '../utilities/timeoutUtils' import { fromSSO } from '@aws-sdk/credential-provider-sso' import { getIAMConnection } from '../../auth/utils' +import { NodeHttpHandler } from '@smithy/node-http-handler' +import type { UserAgent } from '@aws-sdk/types' export type LambdaClient = ClassToInterfaceType @@ -22,7 +50,7 @@ export class DefaultLambdaClient { public constructor( public readonly regionCode: string, - public readonly userAgent: string | undefined = undefined + public readonly userAgent: UserAgent | undefined = undefined ) { this.defaultTimeoutInMs = 5 * 60 * 1000 // 5 minutes (SDK default is 2 minutes) } @@ -30,39 +58,40 @@ export class DefaultLambdaClient { public async deleteFunction(name: string, qualifier?: string): Promise { const sdkClient = await this.createSdkClient() - const response = await sdkClient - .deleteFunction({ + await sdkClient.send( + new DeleteFunctionCommand({ FunctionName: name, Qualifier: qualifier, }) - .promise() - - if (response.$response.error) { - throw response.$response.error - } + ) } - public async invoke(name: string, payload?: _Blob, version?: string): Promise { + public async invoke( + name: string, + payload?: BlobPayloadInputTypes, + version?: string, + logtype: 'Tail' | 'None' = 'Tail' + ): Promise { const sdkClient = await this.createSdkClient() - const response = await sdkClient - .invoke({ + const response = await sdkClient.send( + new InvokeCommand({ FunctionName: name, - LogType: 'Tail', + LogType: logtype, Payload: payload, Qualifier: version, }) - .promise() + ) return response } - public async *listFunctions(): AsyncIterableIterator { + public async *listFunctions(): AsyncIterableIterator { const client = await this.createSdkClient() - const request: Lambda.ListFunctionsRequest = {} + const request: ListFunctionsRequest = {} do { - const response: Lambda.ListFunctionsResponse = await client.listFunctions(request).promise() + const response: ListFunctionsResponse = await client.send(new ListFunctionsCommand(request)) if (response.Functions) { yield* response.Functions @@ -72,12 +101,12 @@ export class DefaultLambdaClient { } while (request.Marker) } - public async getFunction(name: string): Promise { + public async getFunction(name: string): Promise { getLogger().debug(`GetFunction called for function: ${name}`) const client = await this.createSdkClient() try { - const response = await client.getFunction({ FunctionName: name }).promise() + const response = await client.send(new GetFunctionCommand({ FunctionName: name })) // prune `Code` from logs so we don't reveal a signed link to customer resources. getLogger().debug('GetFunction returned response (code section pruned): %O', { ...response, @@ -90,12 +119,12 @@ export class DefaultLambdaClient { } } - public async getLayerVersion(name: string, version: number): Promise { + public async getLayerVersion(name: string, version: number): Promise { getLogger().debug(`getLayerVersion called for LayerName: ${name}, VersionNumber ${version}`) const client = await this.createSdkClient() try { - const response = await client.getLayerVersion({ LayerName: name, VersionNumber: version }).promise() + const response = await client.send(new GetLayerVersionCommand({ LayerName: name, VersionNumber: version })) // prune `Code` from logs so we don't reveal a signed link to customer resources. getLogger().debug('getLayerVersion returned response (code section pruned): %O', { ...response, @@ -108,12 +137,12 @@ export class DefaultLambdaClient { } } - public async *listLayerVersions(name: string): AsyncIterableIterator { + public async *listLayerVersions(name: string): AsyncIterableIterator { const client = await this.createSdkClient() - const request: Lambda.ListLayerVersionsRequest = { LayerName: name } + const request: ListLayerVersionsRequest = { LayerName: name } do { - const response: Lambda.ListLayerVersionsResponse = await client.listLayerVersions(request).promise() + const response: ListLayerVersionsResponse = await client.send(new ListLayerVersionsCommand(request)) if (response.LayerVersions) { yield* response.LayerVersions @@ -123,38 +152,37 @@ export class DefaultLambdaClient { } while (request.Marker) } - public async getFunctionUrlConfigs(name: string): Promise { + public async getFunctionUrlConfigs(name: string): Promise { getLogger().debug(`GetFunctionUrlConfig called for function: ${name}`) const client = await this.createSdkClient() try { - const request = client.listFunctionUrlConfigs({ FunctionName: name }) - const response = await request.promise() + const response = await client.send(new ListFunctionUrlConfigsCommand({ FunctionName: name })) // prune `Code` from logs so we don't reveal a signed link to customer resources. getLogger().debug('GetFunctionUrlConfig returned response (code section pruned): %O', { ...response, Code: 'Pruned', }) - return response.FunctionUrlConfigs + return response.FunctionUrlConfigs ?? [] } catch (e) { throw ToolkitError.chain(e, 'Failed to get Lambda function URLs') } } - public async updateFunctionCode(name: string, zipFile: Uint8Array): Promise { + public async updateFunctionCode(name: string, zipFile: Uint8Array): Promise { getLogger().debug(`updateFunctionCode called for function: ${name}`) const client = await this.createSdkClient() try { - const response = await client - .updateFunctionCode({ + const response = await client.send( + new UpdateFunctionCodeCommand({ FunctionName: name, Publish: true, ZipFile: zipFile, }) - .promise() + ) getLogger().debug('updateFunctionCode returned response: %O', response) - await client.waitFor('functionUpdated', { FunctionName: name }).promise() + await waitUntilFunctionUpdatedV2({ client, maxWaitTime: 300 }, { FunctionName: name }) return response } catch (e) { @@ -164,14 +192,14 @@ export class DefaultLambdaClient { } public async updateFunctionConfiguration( - params: Lambda.UpdateFunctionConfigurationRequest, + params: UpdateFunctionConfigurationRequest, options: { maxRetries?: number initialDelayMs?: number backoffMultiplier?: number waitForUpdate?: boolean } = {} - ): Promise { + ): Promise { const client = await this.createSdkClient() const maxRetries = options.maxRetries ?? 5 const initialDelayMs = options.initialDelayMs ?? 1000 @@ -185,7 +213,7 @@ export class DefaultLambdaClient { // there could be race condition, if function is being updated, wait and retry while (retryCount <= maxRetries) { try { - const response = await client.updateFunctionConfiguration(params).promise() + const response = await client.send(new UpdateFunctionConfigurationCommand(params)) getLogger().debug('updateFunctionConfiguration returned response: %O', response) if (waitForUpdate) { // don't return if wait for result @@ -218,7 +246,9 @@ export class DefaultLambdaClient { let lastUpdateStatus = 'InProgress' while (lastUpdateStatus === 'InProgress') { await new Promise((resolve) => setTimeout(resolve, 1000)) - const response = await client.getFunctionConfiguration({ FunctionName: params.FunctionName }).promise() + const response = await client.send( + new GetFunctionConfigurationCommand({ FunctionName: params.FunctionName }) + ) lastUpdateStatus = response.LastUpdateStatus ?? 'Failed' if (lastUpdateStatus === 'Successful') { return response @@ -236,23 +266,23 @@ export class DefaultLambdaClient { public async publishVersion( name: string, options: { waitForUpdate?: boolean } = {} - ): Promise { + ): Promise { const client = await this.createSdkClient() // return until lambda update is completed const waitForUpdate = options.waitForUpdate ?? false - const response = await client - .publishVersion({ + const response = await client.send( + new PublishVersionCommand({ FunctionName: name, }) - .promise() + ) if (waitForUpdate) { let state = 'Pending' while (state === 'Pending') { await new Promise((resolve) => setTimeout(resolve, 1000)) - const statusResponse = await client - .getFunctionConfiguration({ FunctionName: name, Qualifier: response.Version }) - .promise() + const statusResponse = await client.send( + new GetFunctionConfigurationCommand({ FunctionName: name, Qualifier: response.Version }) + ) state = statusResponse.State ?? 'Failed' if (state === 'Active' || state === 'InActive') { // version creation finished @@ -276,16 +306,36 @@ export class DefaultLambdaClient { ) } - private async createSdkClient(): Promise { - return await globals.sdkClientBuilder.createAwsService( - Lambda, + public async waitForActive( + functionName: string, + waiter?: { maxWaitTime?: number; minDelay?: number; maxDelay?: number } + ): Promise { + const sdkClient = await this.createSdkClient() + + await waitUntilFunctionActiveV2( { - httpOptions: { timeout: this.defaultTimeoutInMs }, - customUserAgent: this.userAgent, + client: sdkClient, + maxWaitTime: waiter?.maxWaitTime ?? 600, + minDelay: waiter?.minDelay ?? 1, + maxDelay: waiter?.maxDelay ?? 120, }, - this.regionCode + { FunctionName: functionName } ) } + + private async createSdkClient(): Promise { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: LambdaSdkClient, + userAgent: !this.userAgent, + clientOptions: { + customUserAgent: this.userAgent, + region: this.regionCode, + requestHandler: new NodeHttpHandler({ + requestTimeout: this.defaultTimeoutInMs, + }), + }, + }) + } } export async function getFunctionWithCredentials(region: string, name: string): Promise { diff --git a/packages/core/src/shared/clients/redshiftClient.ts b/packages/core/src/shared/clients/redshiftClient.ts index a0e98bc405e..5464f58f1d2 100644 --- a/packages/core/src/shared/clients/redshiftClient.ts +++ b/packages/core/src/shared/clients/redshiftClient.ts @@ -4,22 +4,43 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Redshift, RedshiftServerless, RedshiftData } from 'aws-sdk' -import globals from '../extensionGlobals' -import { ClusterCredentials, ClustersMessage, GetClusterCredentialsMessage } from 'aws-sdk/clients/redshift' import { - GetCredentialsRequest, - GetCredentialsResponse, - ListWorkgroupsResponse, -} from 'aws-sdk/clients/redshiftserverless' + ClusterCredentials, + ClustersMessage, + DescribeClustersCommand, + DescribeClustersMessage, + GetClusterCredentialsCommand, + GetClusterCredentialsMessage, + RedshiftClient, +} from '@aws-sdk/client-redshift' import { + DescribeStatementCommand, DescribeStatementRequest, + ExecuteStatementCommand, + GetStatementResultCommand, GetStatementResultRequest, GetStatementResultResponse, + ListDatabasesCommand, + ListDatabasesRequest, ListDatabasesResponse, + ListSchemasCommand, + ListSchemasRequest, ListSchemasResponse, + ListTablesCommand, + ListTablesRequest, ListTablesResponse, -} from 'aws-sdk/clients/redshiftdata' + RedshiftDataClient, +} from '@aws-sdk/client-redshift-data' +import { + GetCredentialsCommand, + GetCredentialsRequest, + GetCredentialsResponse, + ListWorkgroupsCommand, + ListWorkgroupsRequest, + ListWorkgroupsResponse, + RedshiftServerlessClient, +} from '@aws-sdk/client-redshift-serverless' +import globals from '../extensionGlobals' import { ConnectionParams, ConnectionType, RedshiftWarehouseType } from '../../awsService/redshift/models/models' import { sleep } from '../utilities/timeoutUtils' import { SecretsManagerClient } from './secretsManagerClient' @@ -37,21 +58,21 @@ export class DefaultRedshiftClient { public readonly regionCode: string, private readonly redshiftDataClientProvider: ( regionCode: string - ) => Promise = createRedshiftDataClient, - private readonly redshiftClientProvider: (regionCode: string) => Promise = createRedshiftSdkClient, + ) => RedshiftDataClient = createRedshiftDataClient, + private readonly redshiftClientProvider: (regionCode: string) => RedshiftClient = createRedshiftSdkClient, private readonly redshiftServerlessClientProvider: ( regionCode: string - ) => Promise = createRedshiftServerlessSdkClient + ) => RedshiftServerlessClient = createRedshiftServerlessSdkClient ) {} // eslint-disable-next-line require-yield public async describeProvisionedClusters(nextToken?: string): Promise { - const redshiftClient = await this.redshiftClientProvider(this.regionCode) - const request: Redshift.DescribeClustersMessage = { + const redshiftClient = this.redshiftClientProvider(this.regionCode) + const request: DescribeClustersMessage = { Marker: nextToken, MaxRecords: 20, } - const response = await redshiftClient.describeClusters(request).promise() + const response = await redshiftClient.send(new DescribeClustersCommand(request)) if (response.Clusters) { response.Clusters = response.Clusters.filter( (cluster) => cluster.ClusterAvailabilityStatus?.toLowerCase() === 'available' @@ -61,12 +82,12 @@ export class DefaultRedshiftClient { } public async listServerlessWorkgroups(nextToken?: string): Promise { - const redshiftServerlessClient = await this.redshiftServerlessClientProvider(this.regionCode) - const request: RedshiftServerless.ListWorkgroupsRequest = { + const redshiftServerlessClient = this.redshiftServerlessClientProvider(this.regionCode) + const request: ListWorkgroupsRequest = { nextToken: nextToken, maxResults: 20, } - const response = await redshiftServerlessClient.listWorkgroups(request).promise() + const response = await redshiftServerlessClient.send(new ListWorkgroupsCommand(request)) if (response.workgroups) { response.workgroups = response.workgroups.filter( (workgroup) => workgroup.status?.toLowerCase() === 'available' @@ -76,10 +97,10 @@ export class DefaultRedshiftClient { } public async listDatabases(connectionParams: ConnectionParams, nextToken?: string): Promise { - const redshiftDataClient = await this.redshiftDataClientProvider(this.regionCode) + const redshiftDataClient = this.redshiftDataClientProvider(this.regionCode) const warehouseType = connectionParams.warehouseType const warehouseIdentifier = connectionParams.warehouseIdentifier - const input: RedshiftData.ListDatabasesRequest = { + const input: ListDatabasesRequest = { ClusterIdentifier: warehouseType === RedshiftWarehouseType.PROVISIONED ? warehouseIdentifier : undefined, Database: connectionParams.database, DbUser: @@ -94,13 +115,13 @@ export class DefaultRedshiftClient { ? connectionParams.secret : undefined, } - return redshiftDataClient.listDatabases(input).promise() + return redshiftDataClient.send(new ListDatabasesCommand(input)) } public async listSchemas(connectionParams: ConnectionParams, nextToken?: string): Promise { - const redshiftDataClient = await this.redshiftDataClientProvider(this.regionCode) + const redshiftDataClient = this.redshiftDataClientProvider(this.regionCode) const warehouseType = connectionParams.warehouseType const warehouseIdentifier = connectionParams.warehouseIdentifier - const input: RedshiftData.ListSchemasRequest = { + const input: ListSchemasRequest = { ClusterIdentifier: warehouseType === RedshiftWarehouseType.PROVISIONED ? warehouseIdentifier : undefined, Database: connectionParams.database, DbUser: @@ -114,7 +135,7 @@ export class DefaultRedshiftClient { ? connectionParams.secret : undefined, } - return redshiftDataClient.listSchemas(input).promise() + return redshiftDataClient.send(new ListSchemasCommand(input)) } public async listTables( @@ -122,10 +143,10 @@ export class DefaultRedshiftClient { schemaName: string, nextToken?: string ): Promise { - const redshiftDataClient = await this.redshiftDataClientProvider(this.regionCode) + const redshiftDataClient = this.redshiftDataClientProvider(this.regionCode) const warehouseType = connectionParams.warehouseType const warehouseIdentifier = connectionParams.warehouseIdentifier - const input: RedshiftData.ListTablesRequest = { + const input: ListTablesRequest = { ClusterIdentifier: warehouseType === RedshiftWarehouseType.PROVISIONED ? warehouseIdentifier : undefined, DbUser: connectionParams.username && connectionParams.connectionType !== ConnectionType.DatabaseUser @@ -140,7 +161,7 @@ export class DefaultRedshiftClient { ? connectionParams.secret : undefined, } - const ListTablesResponse = redshiftDataClient.listTables(input).promise() + const ListTablesResponse = redshiftDataClient.send(new ListTablesCommand(input)) return ListTablesResponse } @@ -150,11 +171,11 @@ export class DefaultRedshiftClient { nextToken?: string, executionId?: string ): Promise { - const redshiftData = await this.redshiftDataClientProvider(this.regionCode) + const redshiftData = this.redshiftDataClientProvider(this.regionCode) // if executionId is not passed in, that means that we're executing and retrieving the results of the query for the first time. if (!executionId) { - const execution = await redshiftData - .executeStatement({ + const execution = await redshiftData.send( + new ExecuteStatementCommand({ ClusterIdentifier: connectionParams.warehouseType === RedshiftWarehouseType.PROVISIONED ? connectionParams.warehouseIdentifier @@ -174,15 +195,15 @@ export class DefaultRedshiftClient { ? connectionParams.secret : undefined, }) - .promise() + ) executionId = execution.Id type Status = 'RUNNING' | 'FAILED' | 'FINISHED' let status: Status = 'RUNNING' while (status === 'RUNNING') { - const describeStatementResponse = await redshiftData - .describeStatement({ Id: executionId } as DescribeStatementRequest) - .promise() + const describeStatementResponse = await redshiftData.send( + new DescribeStatementCommand({ Id: executionId } as DescribeStatementRequest) + ) if (describeStatementResponse.Status === 'FAILED' || describeStatementResponse.Status === 'FINISHED') { status = describeStatementResponse.Status if (status === 'FAILED') { @@ -198,9 +219,9 @@ export class DefaultRedshiftClient { } } } - const result = await redshiftData - .getStatementResult({ Id: executionId, NextToken: nextToken } as GetStatementResultRequest) - .promise() + const result = await redshiftData.send( + new GetStatementResultCommand({ Id: executionId, NextToken: nextToken } as GetStatementResultRequest) + ) return { statementResultResponse: result, executionId: executionId } as ExecuteQueryResponse } @@ -210,20 +231,20 @@ export class DefaultRedshiftClient { connectionParams: ConnectionParams ): Promise { if (warehouseType === RedshiftWarehouseType.PROVISIONED) { - const redshiftClient = await this.redshiftClientProvider(this.regionCode) + const redshiftClient = this.redshiftClientProvider(this.regionCode) const getClusterCredentialsRequest: GetClusterCredentialsMessage = { DbUser: connectionParams.username!, DbName: connectionParams.database, ClusterIdentifier: connectionParams.warehouseIdentifier, } - return redshiftClient.getClusterCredentials(getClusterCredentialsRequest).promise() + return redshiftClient.send(new GetClusterCredentialsCommand(getClusterCredentialsRequest)) } else { - const redshiftServerless = await this.redshiftServerlessClientProvider(this.regionCode) + const redshiftServerless = this.redshiftServerlessClientProvider(this.regionCode) const getCredentialsRequest: GetCredentialsRequest = { dbName: connectionParams.database, workgroupName: connectionParams.warehouseIdentifier, } - return redshiftServerless.getCredentials(getCredentialsRequest).promise() + return redshiftServerless.send(new GetCredentialsCommand(getCredentialsRequest)) } } public genUniqueId(connectionParams: ConnectionParams): string { @@ -258,13 +279,22 @@ export class DefaultRedshiftClient { } } -async function createRedshiftSdkClient(regionCode: string): Promise { - return await globals.sdkClientBuilder.createAwsService(Redshift, { computeChecksums: true }, regionCode) +function createRedshiftSdkClient(regionCode: string): RedshiftClient { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: RedshiftClient, + clientOptions: { region: regionCode }, + }) } -async function createRedshiftServerlessSdkClient(regionCode: string): Promise { - return await globals.sdkClientBuilder.createAwsService(RedshiftServerless, { computeChecksums: true }, regionCode) +function createRedshiftServerlessSdkClient(regionCode: string): RedshiftServerlessClient { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: RedshiftServerlessClient, + clientOptions: { region: regionCode }, + }) } -async function createRedshiftDataClient(regionCode: string): Promise { - return await globals.sdkClientBuilder.createAwsService(RedshiftData, { computeChecksums: true }, regionCode) +function createRedshiftDataClient(regionCode: string): RedshiftDataClient { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: RedshiftDataClient, + clientOptions: { region: regionCode }, + }) } diff --git a/packages/core/src/shared/clients/sagemaker.ts b/packages/core/src/shared/clients/sagemaker.ts index 8a8e138dd85..3b68c86cfda 100644 --- a/packages/core/src/shared/clients/sagemaker.ts +++ b/packages/core/src/shared/clients/sagemaker.ts @@ -1,11 +1,11 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' import { AppDetails, + AppType, CreateAppCommand, CreateAppCommandInput, CreateAppCommandOutput, @@ -32,6 +32,12 @@ import { UpdateSpaceCommandOutput, paginateListApps, paginateListSpaces, + ListClustersCommandInput, + DescribeClusterCommand, + DescribeClusterCommandInput, + DescribeClusterCommandOutput, + ClusterSummary, + paginateListClusters, } from '@amzn/sagemaker-client' import { isEmpty } from 'lodash' import { sleep } from '../utilities/timeoutUtils' @@ -43,19 +49,66 @@ import { InstanceTypeInsufficientMemory, InstanceTypeInsufficientMemoryMessage, InstanceTypeNotSelectedMessage, + RemoteAccess, } from '../../awsService/sagemaker/constants' import { getDomainSpaceKey } from '../../awsService/sagemaker/utils' import { getLogger } from '../logger/logger' import { ToolkitError } from '../errors' -import { yes, no, continueText, cancel } from '../localizedText' +import { continueText, cancel } from '../localizedText' +import { showConfirmationMessage } from '../utilities/messages' +import { AwsCredentialIdentity } from '@aws-sdk/types' +import globals from '../extensionGlobals' +import { HyperpodCluster } from './kubectlClient' +import { EKSClient } from '@aws-sdk/client-eks' +import { DevSettings } from '../settings' + +const appTypeSettingsMap: Record = { + [AppType.JupyterLab as string]: 'JupyterLabAppSettings', + [AppType.CodeEditor as string]: 'CodeEditorAppSettings', +} as const + +export const waitForAppConfig = { + softTimeoutRetries: 12, + hardTimeoutRetries: 120, + intervalMs: 5000, +} export interface SagemakerSpaceApp extends SpaceDetails { App?: AppDetails DomainSpaceKey: string } + export class SagemakerClient extends ClientWrapper { - public constructor(public override readonly regionCode: string) { - super(regionCode, SageMakerClient, true) + public constructor( + public override readonly regionCode: string, + private readonly credentialsProvider?: () => Promise + ) { + super(regionCode, SageMakerClient) + } + + protected override getClient(ignoreCache: boolean = false) { + if (!this.client || ignoreCache) { + const devSettings = DevSettings.instance + const customEndpoint = devSettings.get('endpoints', {})['sagemaker'] + const endpoint = customEndpoint || `https://sagemaker.${this.regionCode}.amazonaws.com` + const args = { + serviceClient: SageMakerClient, + region: this.regionCode, + clientOptions: { + endpoint: endpoint, + region: this.regionCode, + ...(this.credentialsProvider && { credentials: this.credentialsProvider }), + }, + } + this.client = globals.sdkClientBuilderV3.createAwsService(args) + } + return this.client + } + + public override dispose() { + getLogger().debug('SagemakerClient: Disposing client %O', this.client) + this.client?.destroy() + this.client = undefined } public listSpaces(request: ListSpacesCommandInput = {}): AsyncCollection { @@ -92,7 +145,14 @@ export class SagemakerClient extends ClientWrapper { return this.makeRequest(DeleteAppCommand, request) } - public async startSpace(spaceName: string, domainId: string) { + public async listAppForSpace(domainId: string, spaceName: string): Promise { + const appsList = await this.listApps({ DomainIdEquals: domainId, SpaceNameEquals: spaceName }) + .flatten() + .promise() + return appsList[0] // At most one App for one SagemakerSpace + } + + public async startSpace(spaceName: string, domainId: string, skipInstanceTypePrompts: boolean = false) { let spaceDetails: DescribeSpaceCommandOutput // Get existing space details @@ -107,13 +167,13 @@ export class SagemakerClient extends ClientWrapper { // Get app type const appType = spaceDetails.SpaceSettings?.AppType - if (appType !== 'JupyterLab' && appType !== 'CodeEditor') { + if (!appType || !(appType in appTypeSettingsMap)) { throw new ToolkitError(`Unsupported AppType "${appType}" for space "${spaceName}"`) } // Get app resource spec const requestedResourceSpec = - appType === 'JupyterLab' + appType === AppType.JupyterLab ? spaceDetails.SpaceSettings?.JupyterLabAppSettings?.DefaultResourceSpec : spaceDetails.SpaceSettings?.CodeEditorAppSettings?.DefaultResourceSpec @@ -121,47 +181,74 @@ export class SagemakerClient extends ClientWrapper { // Is InstanceType defined and has enough memory? if (instanceType && instanceType in InstanceTypeInsufficientMemory) { - // Prompt user to select one with sufficient memory (1 level up from their chosen one) - const response = await vscode.window.showErrorMessage( - InstanceTypeInsufficientMemoryMessage( - spaceDetails.SpaceName || '', - instanceType, - InstanceTypeInsufficientMemory[instanceType] - ), - yes, - no - ) + if (skipInstanceTypePrompts) { + // User already consented, upgrade automatically + instanceType = InstanceTypeInsufficientMemory[instanceType] + } else { + // Prompt user to select one with sufficient memory (1 level up from their chosen one) + const confirmed = await showConfirmationMessage({ + prompt: InstanceTypeInsufficientMemoryMessage( + spaceDetails.SpaceName || '', + instanceType, + InstanceTypeInsufficientMemory[instanceType] + ), + confirm: 'Restart Space and Connect', + cancel: 'Cancel', + type: 'warning', + }) - if (response === no) { - throw new ToolkitError('InstanceType has insufficient memory.', { code: InstanceTypeError }) - } + if (!confirmed) { + throw new ToolkitError('InstanceType has insufficient memory.', { code: InstanceTypeError }) + } - instanceType = InstanceTypeInsufficientMemory[instanceType] + instanceType = InstanceTypeInsufficientMemory[instanceType] + } } else if (!instanceType) { - // Prompt user to select the minimum supported instance type - const response = await vscode.window.showErrorMessage( - InstanceTypeNotSelectedMessage(spaceDetails.SpaceName || ''), - continueText, - cancel - ) + if (skipInstanceTypePrompts) { + // User already consented, use minimum + instanceType = InstanceTypeMinimum + } else { + // Prompt user to select the minimum supported instance type + const confirmed = await showConfirmationMessage({ + prompt: InstanceTypeNotSelectedMessage(spaceDetails.SpaceName || ''), + confirm: continueText, + cancel: cancel, + type: 'warning', + }) - if (response === cancel) { - throw new ToolkitError('InstanceType not defined.', { code: InstanceTypeError }) - } + if (!confirmed) { + throw new ToolkitError('InstanceType not defined.', { code: InstanceTypeError }) + } - instanceType = InstanceTypeMinimum + instanceType = InstanceTypeMinimum + } } - // Get remote access flag - if (!spaceDetails.SpaceSettings?.RemoteAccess || spaceDetails.SpaceSettings?.RemoteAccess === 'DISABLED') { + // First, update the space if needed + const needsRemoteAccess = + !spaceDetails.SpaceSettings?.RemoteAccess || + spaceDetails.SpaceSettings?.RemoteAccess === RemoteAccess.DISABLED + const instanceTypeChanged = requestedResourceSpec?.InstanceType !== instanceType + + if (needsRemoteAccess || instanceTypeChanged) { + const updateSpaceRequest: UpdateSpaceCommandInput = { + DomainId: domainId, + SpaceName: spaceName, + SpaceSettings: { + ...(needsRemoteAccess && { RemoteAccess: RemoteAccess.ENABLED }), + ...(instanceTypeChanged && { + [appTypeSettingsMap[appType]]: { + DefaultResourceSpec: { + InstanceType: instanceType, + }, + }, + }), + }, + } + try { - await this.updateSpace({ - DomainId: domainId, - SpaceName: spaceName, - SpaceSettings: { - RemoteAccess: 'ENABLED', - }, - }) + getLogger().debug('SagemakerClient: Updating space: domainId=%s, spaceName=%s', domainId, spaceName) + await this.updateSpace(updateSpaceRequest) await this.waitForSpaceInService(spaceName, domainId) } catch (err) { throw this.handleStartSpaceError(err) @@ -185,6 +272,7 @@ export class SagemakerClient extends ClientWrapper { ? { ...resourceSpec, EnvironmentArn: undefined, EnvironmentVersionArn: undefined } : resourceSpec + // Second, create the App const createAppRequest: CreateAppCommandInput = { DomainId: domainId, SpaceName: spaceName, @@ -194,33 +282,44 @@ export class SagemakerClient extends ClientWrapper { } try { + getLogger().debug('SagemakerClient: Creating app: domainId=%s, spaceName=%s', domainId, spaceName) await this.createApp(createAppRequest) } catch (err) { throw this.handleStartSpaceError(err) } } - public async fetchSpaceAppsAndDomains(): Promise< - [Map, Map] - > { - try { - const appMap: Map = await this.listApps() - .flatten() - .filter((app) => !!app.DomainId && !!app.SpaceName) - .filter((app) => app.AppType === 'JupyterLab' || app.AppType === 'CodeEditor') - .toMap((app) => getDomainSpaceKey(app.DomainId || '', app.SpaceName || '')) - - const spaceApps: Map = await this.listSpaces() - .flatten() - .filter((space) => !!space.DomainId && !!space.SpaceName) - .map((space) => { - const key = getDomainSpaceKey(space.DomainId || '', space.SpaceName || '') - return { ...space, App: appMap.get(key), DomainSpaceKey: key } - }) - .toMap((space) => getDomainSpaceKey(space.DomainId || '', space.SpaceName || '')) + public async listSpaceApps(domainId?: string): Promise> { + // Create options object conditionally if domainId is provided + const options = domainId ? { DomainIdEquals: domainId } : undefined + + const appMap: Map = await this.listApps(options) + .flatten() + .filter((app) => !!app.DomainId && !!app.SpaceName) + .filter((app) => app.AppType === AppType.JupyterLab || app.AppType === AppType.CodeEditor) + .toMap((app) => getDomainSpaceKey(app.DomainId || '', app.SpaceName || '')) + + const spaceApps: Map = await this.listSpaces(options) + .flatten() + .filter((space) => !!space.DomainId && !!space.SpaceName) + .map((space) => { + const key = getDomainSpaceKey(space.DomainId || '', space.SpaceName || '') + return { ...space, App: appMap.get(key), DomainSpaceKey: key } + }) + .toMap((space) => getDomainSpaceKey(space.DomainId || '', space.SpaceName || '')) + return spaceApps + } + public async fetchSpaceAppsAndDomains( + domainId?: string, + filterSmusDomains: boolean = true + ): Promise<[Map, Map]> { + try { + const spaceApps = await this.listSpaceApps(domainId) // Get de-duped list of domain IDs for all of the spaces - const domainIds: string[] = [...new Set([...spaceApps].map(([_, spaceApp]) => spaceApp.DomainId || ''))] + const domainIds: string[] = domainId + ? [domainId] + : [...new Set([...spaceApps].map(([_, spaceApp]) => spaceApp.DomainId || ''))] // Get details for each domain const domains: [string, DescribeDomainResponse][] = await Promise.all( @@ -235,9 +334,11 @@ export class SagemakerClient extends ClientWrapper { const filteredSpaceApps = new Map( [...spaceApps] - // Filter out SageMaker Unified Studio domains - .filter(([_, spaceApp]) => - isEmpty(domainsMap.get(spaceApp.DomainId || '')?.DomainSettings?.UnifiedStudioSettings) + // Filter out SageMaker Unified Studio domains only if filterSmusDomains is true + .filter( + ([_, spaceApp]) => + !filterSmusDomains || + isEmpty(domainsMap.get(spaceApp.DomainId || '')?.DomainSettings?.UnifiedStudioSettings) ) ) @@ -280,10 +381,9 @@ export class SagemakerClient extends ClientWrapper { domainId: string, spaceName: string, appType: string, - maxRetries = 30, - intervalMs = 5000 + progress?: vscode.Progress<{ message?: string; increment?: number }> ): Promise { - for (let attempt = 0; attempt < maxRetries; attempt++) { + for (let attempt = 0; attempt < waitForAppConfig.hardTimeoutRetries; attempt++) { const { Status } = await this.describeApp({ DomainId: domainId, SpaceName: spaceName, @@ -299,7 +399,13 @@ export class SagemakerClient extends ClientWrapper { throw new ToolkitError(`App failed to start. Status: ${Status}`) } - await sleep(intervalMs) + if (attempt === waitForAppConfig.softTimeoutRetries) { + progress?.report({ + message: `Starting the space is taking longer than usual. The space will connect when ready`, + }) + } + + await sleep(waitForAppConfig.intervalMs) } throw new ToolkitError(`Timed out waiting for app "${spaceName}" to reach "InService" status.`) @@ -315,4 +421,61 @@ export class SagemakerClient extends ClientWrapper { throw err } } + + public listClusters(request: ListClustersCommandInput = {}): AsyncCollection { + // @ts-ignore: Suppressing type mismatch on paginator return type + return this.makePaginatedRequest(paginateListClusters, request, (page) => page.ClusterSummaries) + } + + public describeCluster(request: DescribeClusterCommandInput): Promise { + return this.makeRequest(DescribeClusterCommand, request) + } + + public async listHyperpodClusters(): Promise { + const clusterSummaries = await this.listClusters().flatten().promise() + const clusters: HyperpodCluster[] = [] + + for (const summary of clusterSummaries) { + clusters.push(await this.getHyperpodCluster(summary.ClusterName!)) + } + return clusters + } + + async getHyperpodCluster(clusterName: string): Promise { + const response = await this.describeCluster({ ClusterName: clusterName }) + + if (!response.ClusterArn) { + throw new Error(`Cluster ${clusterName} not found`) + } + + const orchestrator = response.Orchestrator + let eksClusterName: string | undefined + let eksClusterArn: string | undefined + + if (orchestrator?.Eks) { + eksClusterName = orchestrator.Eks.ClusterArn?.split('/').pop() + eksClusterArn = orchestrator.Eks.ClusterArn + } + + return { + clusterName: response.ClusterName!, + clusterArn: response.ClusterArn, + status: response.ClusterStatus!, + eksClusterName, + eksClusterArn, + regionCode: this.regionCode, + } + } + + public getEKSClient(ignoreCache: boolean = false) { + const args = { + serviceClient: EKSClient as any, + region: this.regionCode, + clientOptions: { + region: this.regionCode, + ...(this.credentialsProvider && { credentials: this.credentialsProvider }), + }, + } + return globals.sdkClientBuilderV3.createAwsService(args) as EKSClient + } } diff --git a/packages/core/src/shared/clients/schemaClient.ts b/packages/core/src/shared/clients/schemaClient.ts index 1aa38621a75..238b0d46810 100644 --- a/packages/core/src/shared/clients/schemaClient.ts +++ b/packages/core/src/shared/clients/schemaClient.ts @@ -3,7 +3,33 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Schemas } from 'aws-sdk' +import { + DescribeCodeBindingCommand, + DescribeCodeBindingResponse, + DescribeSchemaCommand, + DescribeSchemaResponse, + GetCodeBindingSourceCommand, + GetCodeBindingSourceResponse, + ListRegistriesCommand, + ListRegistriesRequest, + ListRegistriesResponse, + ListSchemasCommand, + ListSchemasRequest, + ListSchemasResponse, + ListSchemaVersionsCommand, + ListSchemaVersionsRequest, + ListSchemaVersionsResponse, + PutCodeBindingCommand, + PutCodeBindingResponse, + RegistrySummary, + SchemasClient, + SchemaSummary, + SchemaVersionSummary, + SearchSchemasCommand, + SearchSchemasRequest, + SearchSchemasResponse, + SearchSchemaSummary, +} from '@aws-sdk/client-schemas' import globals from '../extensionGlobals' import { ClassToInterfaceType } from '../utilities/tsUtils' @@ -12,13 +38,13 @@ export type SchemaClient = ClassToInterfaceType export class DefaultSchemaClient { public constructor(public readonly regionCode: string) {} - public async *listRegistries(): AsyncIterableIterator { - const client = await this.createSdkClient() + public async *listRegistries(): AsyncIterableIterator { + const client = this.createSdkClient() - const request: Schemas.ListRegistriesRequest = {} + const request: ListRegistriesRequest = {} do { - const response: Schemas.ListRegistriesResponse = await client.listRegistries(request).promise() + const response: ListRegistriesResponse = await client.send(new ListRegistriesCommand(request)) if (response.Registries) { yield* response.Registries @@ -28,15 +54,15 @@ export class DefaultSchemaClient { } while (request.NextToken) } - public async *listSchemas(registryName: string): AsyncIterableIterator { - const client = await this.createSdkClient() + public async *listSchemas(registryName: string): AsyncIterableIterator { + const client = this.createSdkClient() - const request: Schemas.ListSchemasRequest = { + const request: ListSchemasRequest = { RegistryName: registryName, } do { - const response: Schemas.ListSchemasResponse = await client.listSchemas(request).promise() + const response: ListSchemasResponse = await client.send(new ListSchemasCommand(request)) if (response.Schemas) { yield* response.Schemas @@ -50,31 +76,31 @@ export class DefaultSchemaClient { registryName: string, schemaName: string, schemaVersion?: string - ): Promise { - const client = await this.createSdkClient() + ): Promise { + const client = this.createSdkClient() - return await client - .describeSchema({ + return await client.send( + new DescribeSchemaCommand({ RegistryName: registryName, SchemaName: schemaName, SchemaVersion: schemaVersion, }) - .promise() + ) } public async *listSchemaVersions( registryName: string, schemaName: string - ): AsyncIterableIterator { - const client = await this.createSdkClient() + ): AsyncIterableIterator { + const client = this.createSdkClient() - const request: Schemas.ListSchemaVersionsRequest = { + const request: ListSchemaVersionsRequest = { RegistryName: registryName, SchemaName: schemaName, } do { - const response: Schemas.ListSchemaVersionsResponse = await client.listSchemaVersions(request).promise() + const response: ListSchemaVersionsResponse = await client.send(new ListSchemaVersionsCommand(request)) if (response.SchemaVersions) { yield* response.SchemaVersions @@ -84,19 +110,16 @@ export class DefaultSchemaClient { } while (request.NextToken) } - public async *searchSchemas( - keywords: string, - registryName: string - ): AsyncIterableIterator { - const client = await this.createSdkClient() + public async *searchSchemas(keywords: string, registryName: string): AsyncIterableIterator { + const client = this.createSdkClient() - const request: Schemas.SearchSchemasRequest = { + const request: SearchSchemasRequest = { Keywords: keywords, RegistryName: registryName, } do { - const response: Schemas.SearchSchemasResponse = await client.searchSchemas(request).promise() + const response: SearchSchemasResponse = await client.send(new SearchSchemasCommand(request)) if (response.Schemas) { yield* response.Schemas @@ -111,17 +134,17 @@ export class DefaultSchemaClient { registryName: string, schemaName: string, schemaVersion: string - ): Promise { - const client = await this.createSdkClient() + ): Promise { + const client = this.createSdkClient() - return await client - .getCodeBindingSource({ + return await client.send( + new GetCodeBindingSourceCommand({ Language: language, RegistryName: registryName, SchemaName: schemaName, SchemaVersion: schemaVersion, }) - .promise() + ) } public async putCodeBinding( @@ -129,37 +152,40 @@ export class DefaultSchemaClient { registryName: string, schemaName: string, schemaVersion: string - ): Promise { - const client = await this.createSdkClient() + ): Promise { + const client = this.createSdkClient() - return await client - .putCodeBinding({ + return await client.send( + new PutCodeBindingCommand({ Language: language, RegistryName: registryName, SchemaName: schemaName, SchemaVersion: schemaVersion, }) - .promise() + ) } public async describeCodeBinding( language: string, registryName: string, schemaName: string, schemaVersion: string - ): Promise { - const client = await this.createSdkClient() + ): Promise { + const client = this.createSdkClient() - return await client - .describeCodeBinding({ + return await client.send( + new DescribeCodeBindingCommand({ Language: language, RegistryName: registryName, SchemaName: schemaName, SchemaVersion: schemaVersion, }) - .promise() + ) } - private async createSdkClient(): Promise { - return await globals.sdkClientBuilder.createAwsService(Schemas, undefined, this.regionCode) + private createSdkClient(): SchemasClient { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: SchemasClient, + clientOptions: { region: this.regionCode }, + }) } } diff --git a/packages/core/src/shared/clients/secretsManagerClient.ts b/packages/core/src/shared/clients/secretsManagerClient.ts index 4696999495d..af9af46d55d 100644 --- a/packages/core/src/shared/clients/secretsManagerClient.ts +++ b/packages/core/src/shared/clients/secretsManagerClient.ts @@ -3,14 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SecretsManager } from 'aws-sdk' -import globals from '../extensionGlobals' import { + CreateSecretCommand, CreateSecretRequest, CreateSecretResponse, + ListSecretsCommand, ListSecretsRequest, ListSecretsResponse, -} from 'aws-sdk/clients/secretsmanager' + SecretsManagerClient as SecretsManagerSdkClient, +} from '@aws-sdk/client-secrets-manager' +import globals from '../extensionGlobals' import { productName } from '../constants' export class SecretsManagerClient { @@ -18,7 +20,7 @@ export class SecretsManagerClient { public readonly regionCode: string, private readonly secretsManagerClientProvider: ( regionCode: string - ) => Promise = createSecretsManagerClient + ) => SecretsManagerSdkClient = createSecretsManagerClient ) {} /** @@ -27,7 +29,7 @@ export class SecretsManagerClient { * @returns a list of the secrets */ public async listSecrets(filter: string): Promise { - const secretsManagerClient = await this.secretsManagerClientProvider(this.regionCode) + const secretsManagerClient = this.secretsManagerClientProvider(this.regionCode) const request: ListSecretsRequest = { IncludePlannedDeletion: false, Filters: [ @@ -38,11 +40,11 @@ export class SecretsManagerClient { ], SortOrder: 'desc', } - return secretsManagerClient.listSecrets(request).promise() + return secretsManagerClient.send(new ListSecretsCommand(request)) } public async createSecret(secretString: string, username: string, password: string): Promise { - const secretsManagerClient = await this.secretsManagerClientProvider(this.regionCode) + const secretsManagerClient = this.secretsManagerClientProvider(this.regionCode) const request: CreateSecretRequest = { Description: `Database secret created with ${productName}`, Name: secretString ? secretString : '', @@ -59,10 +61,13 @@ export class SecretsManagerClient { ], ForceOverwriteReplicaSecret: true, } - return secretsManagerClient.createSecret(request).promise() + return secretsManagerClient.send(new CreateSecretCommand(request)) } } -async function createSecretsManagerClient(regionCode: string): Promise { - return await globals.sdkClientBuilder.createAwsService(SecretsManager, { computeChecksums: true }, regionCode) +function createSecretsManagerClient(regionCode: string): SecretsManagerSdkClient { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: SecretsManagerSdkClient, + clientOptions: { region: regionCode }, + }) } diff --git a/packages/core/src/shared/clients/ssmDocumentClient.ts b/packages/core/src/shared/clients/ssmDocumentClient.ts index 774b9a2b2fc..581cb0bc219 100644 --- a/packages/core/src/shared/clients/ssmDocumentClient.ts +++ b/packages/core/src/shared/clients/ssmDocumentClient.ts @@ -3,7 +3,36 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SSM } from 'aws-sdk' +import { + CreateDocumentCommand, + CreateDocumentRequest, + CreateDocumentResult, + DeleteDocumentCommand, + DeleteDocumentRequest, + DeleteDocumentResult, + DescribeDocumentCommand, + DescribeDocumentRequest, + DescribeDocumentResult, + DocumentFormat, + DocumentIdentifier, + DocumentVersionInfo, + GetDocumentCommand, + GetDocumentRequest, + GetDocumentResult, + ListDocumentsCommand, + ListDocumentsRequest, + ListDocumentsResult, + ListDocumentVersionsCommand, + ListDocumentVersionsRequest, + ListDocumentVersionsResult, + SSMClient, + UpdateDocumentCommand, + UpdateDocumentDefaultVersionCommand, + UpdateDocumentDefaultVersionRequest, + UpdateDocumentDefaultVersionResult, + UpdateDocumentRequest, + UpdateDocumentResult, +} from '@aws-sdk/client-ssm' import globals from '../extensionGlobals' import { ClassToInterfaceType } from '../utilities/tsUtils' @@ -12,23 +41,21 @@ export type SsmDocumentClient = ClassToInterfaceType export class DefaultSsmDocumentClient { public constructor(public readonly regionCode: string) {} - public async deleteDocument(documentName: string): Promise { - const client = await this.createSdkClient() + public async deleteDocument(documentName: string): Promise { + const client = this.createSdkClient() - const request: SSM.Types.DeleteDocumentRequest = { + const request: DeleteDocumentRequest = { Name: documentName, } - return await client.deleteDocument(request).promise() + return await client.send(new DeleteDocumentCommand(request)) } - public async *listDocuments( - request: SSM.Types.ListDocumentsRequest = {} - ): AsyncIterableIterator { - const client = await this.createSdkClient() + public async *listDocuments(request: ListDocumentsRequest = {}): AsyncIterableIterator { + const client = this.createSdkClient() do { - const response: SSM.Types.ListDocumentsResult = await client.listDocuments(request).promise() + const response: ListDocumentsResult = await client.send(new ListDocumentsCommand(request)) if (response.DocumentIdentifiers) { yield* response.DocumentIdentifiers @@ -38,15 +65,15 @@ export class DefaultSsmDocumentClient { } while (request.NextToken) } - public async *listDocumentVersions(documentName: string): AsyncIterableIterator { - const client = await this.createSdkClient() + public async *listDocumentVersions(documentName: string): AsyncIterableIterator { + const client = this.createSdkClient() - const request: SSM.Types.ListDocumentVersionsRequest = { + const request: ListDocumentVersionsRequest = { Name: documentName, } do { - const response: SSM.Types.ListDocumentVersionsResult = await client.listDocumentVersions(request).promise() + const response: ListDocumentVersionsResult = await client.send(new ListDocumentVersionsCommand(request)) if (response.DocumentVersions) { yield* response.DocumentVersions @@ -56,60 +83,63 @@ export class DefaultSsmDocumentClient { } while (request.NextToken) } - public async describeDocument(documentName: string, documentVersion?: string): Promise { - const client = await this.createSdkClient() + public async describeDocument(documentName: string, documentVersion?: string): Promise { + const client = this.createSdkClient() - const request: SSM.Types.DescribeDocumentRequest = { + const request: DescribeDocumentRequest = { Name: documentName, DocumentVersion: documentVersion, } - return await client.describeDocument(request).promise() + return await client.send(new DescribeDocumentCommand(request)) } public async getDocument( documentName: string, documentVersion?: string, - documentFormat?: string - ): Promise { - const client = await this.createSdkClient() + documentFormat?: DocumentFormat + ): Promise { + const client = this.createSdkClient() - const request: SSM.Types.GetDocumentRequest = { + const request: GetDocumentRequest = { Name: documentName, DocumentVersion: documentVersion, DocumentFormat: documentFormat, } - return await client.getDocument(request).promise() + return await client.send(new GetDocumentCommand(request)) } - public async createDocument(request: SSM.Types.CreateDocumentRequest): Promise { - const client = await this.createSdkClient() + public async createDocument(request: CreateDocumentRequest): Promise { + const client = this.createSdkClient() - return await client.createDocument(request).promise() + return await client.send(new CreateDocumentCommand(request)) } - public async updateDocument(request: SSM.Types.UpdateDocumentRequest): Promise { - const client = await this.createSdkClient() + public async updateDocument(request: UpdateDocumentRequest): Promise { + const client = this.createSdkClient() - return await client.updateDocument(request).promise() + return await client.send(new UpdateDocumentCommand(request)) } public async updateDocumentVersion( documentName: string, documentVersion: string - ): Promise { - const client = await this.createSdkClient() + ): Promise { + const client = this.createSdkClient() - const request: SSM.Types.UpdateDocumentDefaultVersionRequest = { + const request: UpdateDocumentDefaultVersionRequest = { Name: documentName, DocumentVersion: documentVersion, } - return await client.updateDocumentDefaultVersion(request).promise() + return await client.send(new UpdateDocumentDefaultVersionCommand(request)) } - private async createSdkClient(): Promise { - return await globals.sdkClientBuilder.createAwsService(SSM, undefined, this.regionCode) + private createSdkClient(): SSMClient { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: SSMClient, + clientOptions: { region: this.regionCode }, + }) } } diff --git a/packages/core/src/shared/clients/stsClient.ts b/packages/core/src/shared/clients/stsClient.ts index a090a846bf8..fdc11e57c30 100644 --- a/packages/core/src/shared/clients/stsClient.ts +++ b/packages/core/src/shared/clients/stsClient.ts @@ -3,38 +3,66 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { STS } from 'aws-sdk' +import { STSClient, AssumeRoleCommand, GetCallerIdentityCommand } from '@aws-sdk/client-sts' +import type { AssumeRoleRequest, AssumeRoleResponse, GetCallerIdentityResponse } from '@aws-sdk/client-sts' +import { AwsCredentialIdentityProvider } from '@smithy/types' import { Credentials } from '@aws-sdk/types' import globals from '../extensionGlobals' import { ClassToInterfaceType } from '../utilities/tsUtils' +// Extended response type that includes captured HTTP headers (added by global middleware) +export interface GetCallerIdentityResponseWithHeaders extends GetCallerIdentityResponse { + $httpHeaders?: Record +} + +export type { GetCallerIdentityResponse } export type StsClient = ClassToInterfaceType + +// Helper function to convert Credentials to AwsCredentialIdentityProvider +function toCredentialProvider(credentials: Credentials | AwsCredentialIdentityProvider): AwsCredentialIdentityProvider { + if (typeof credentials === 'function') { + return credentials + } + // Convert static credentials to provider function + return async () => credentials +} + export class DefaultStsClient { public constructor( public readonly regionCode: string, - private readonly credentials?: Credentials + private readonly credentials?: Credentials | AwsCredentialIdentityProvider, + private readonly endpointUrl?: string ) {} - public async assumeRole(request: STS.AssumeRoleRequest): Promise { - const sdkClient = await this.createSdkClient() - const response = await sdkClient.assumeRole(request).promise() + public async assumeRole(request: AssumeRoleRequest): Promise { + const sdkClient = this.createSdkClient() + const response = await sdkClient.send(new AssumeRoleCommand(request)) return response } - public async getCallerIdentity(): Promise { - const sdkClient = await this.createSdkClient() - const response = await sdkClient.getCallerIdentity().promise() + public async getCallerIdentity(): Promise { + const sdkClient = this.createSdkClient() + // Note: $httpHeaders are added by global middleware in awsClientBuilderV3 + const response = await sdkClient.send(new GetCallerIdentityCommand({})) return response } - private async createSdkClient(): Promise { - return await globals.sdkClientBuilder.createAwsService( - STS, - { - credentials: this.credentials, - stsRegionalEndpoints: 'regional', - }, - this.regionCode - ) + private createSdkClient(): STSClient { + const clientOptions: { region: string; endpoint?: string; credentials?: AwsCredentialIdentityProvider } = { + region: this.regionCode, + } + + if (this.endpointUrl) { + clientOptions.endpoint = this.endpointUrl + } + + if (this.credentials) { + clientOptions.credentials = toCredentialProvider(this.credentials) + } + + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: STSClient, + clientOptions, + }) } } diff --git a/packages/core/src/shared/cloudformation/cloudformation.ts b/packages/core/src/shared/cloudformation/cloudformation.ts index 690a5aee73c..fc815ac6033 100644 --- a/packages/core/src/shared/cloudformation/cloudformation.ts +++ b/packages/core/src/shared/cloudformation/cloudformation.ts @@ -19,6 +19,8 @@ export const LAMBDA_FUNCTION_TYPE = 'AWS::Lambda::Function' // eslint-disable-li export const LAMBDA_LAYER_TYPE = 'AWS::Lambda::LayerVersion' // eslint-disable-line @typescript-eslint/naming-convention export const LAMBDA_URL_TYPE = 'AWS::Lambda::Url' // eslint-disable-line @typescript-eslint/naming-convention export const SERVERLESS_LAYER_TYPE = 'AWS::Serverless::LayerVersion' // eslint-disable-line @typescript-eslint/naming-convention +export const SERVERLESS_CAPACITY_PROVIDER_TYPE = 'AWS::Serverless::CapacityProvider' // eslint-disable-line @typescript-eslint/naming-convention +export const LAMBDA_CAPACITY_PROVIDER_TYPE = 'AWS::Lambda::CapacityProvider' // eslint-disable-line @typescript-eslint/naming-convention export const serverlessTableType = 'AWS::Serverless::SimpleTable' export const s3BucketType = 'AWS::S3::Bucket' diff --git a/packages/core/src/shared/env/resolveEnv.ts b/packages/core/src/shared/env/resolveEnv.ts index 7b1b4bc31cb..c15922fe0c9 100644 --- a/packages/core/src/shared/env/resolveEnv.ts +++ b/packages/core/src/shared/env/resolveEnv.ts @@ -19,6 +19,7 @@ import { IamConnection } from '../../auth/connection' import { asEnvironmentVariables } from '../../auth/credentials/utils' import { getIAMConnection } from '../../auth/utils' import { ChildProcess } from '../utilities/processUtils' +import globals from '../extensionGlobals' let unixShellEnvPromise: Promise | undefined = undefined let envCacheExpireTime: number @@ -65,7 +66,8 @@ function getSystemShellUnixLike(env: IProcessEnvironment): string { export async function injectCredentials(conn: IamConnection, env = process.env): Promise { const creds = await conn.getCredentials() - return { ...env, ...asEnvironmentVariables(creds) } + const endpointUrl = globals.awsContext.getCredentialEndpointUrl() + return { ...env, ...asEnvironmentVariables(creds, endpointUrl) } } export interface getEnvOptions { diff --git a/packages/core/src/shared/errors.ts b/packages/core/src/shared/errors.ts index 5d114043be3..292a6cc5f5a 100644 --- a/packages/core/src/shared/errors.ts +++ b/packages/core/src/shared/errors.ts @@ -375,6 +375,7 @@ export function getTelemetryResult(error: unknown | undefined): Result { * Examples: * - "Failed to save c:/fooß/bar/baz.txt" => "Failed to save c:/xß/x/x.txt" * - "EPERM for dir c:/Users/user1/.aws/sso/cache/abc123.json" => "EPERM for dir c:/Users/x/.aws/sso/cache/x.json" + * - "Error with profile my-profile" => "Error with profile [REDACTED]" */ export function scrubNames(s: string, username?: string) { let r = '' @@ -405,6 +406,10 @@ export function scrubNames(s: string, username?: string) { s = s.replaceAll(username, 'x') } + // Remove profile names that might appear in error messages + // Matches "profile" followed by optional punctuation and the profile name + s = s.replace(/(profile)\s*[:'"]?\s*([\w-]+)['"']?/gi, '$1 [REDACTED]') + // Replace contiguous whitespace with 1 space. s = s.replace(/\s+/g, ' ') @@ -600,6 +605,10 @@ export function isAwsError(error: unknown): error is AWSError & { error_descript return error instanceof Error && hasCode(error) && hasTime(error) } +export function isServiceException(error: unknown): error is ServiceException { + return error instanceof ServiceException +} + export function hasCode(error: T): error is T & { code: string } { return typeof (error as { code?: unknown }).code === 'string' } diff --git a/packages/core/src/shared/extensionUtilities.ts b/packages/core/src/shared/extensionUtilities.ts index 80bedf1e0f6..b8b5780c612 100644 --- a/packages/core/src/shared/extensionUtilities.ts +++ b/packages/core/src/shared/extensionUtilities.ts @@ -188,7 +188,7 @@ export function isCloud9(flavor: 'classic' | 'codecatalyst' | 'any' = 'any'): bo * @param appName to identify the proper SM instance * @returns true if the current system is SageMaker(SMAI or SMUS) */ -export function isSageMaker(appName: 'SMAI' | 'SMUS' = 'SMAI'): boolean { +export function isSageMaker(appName: 'SMAI' | 'SMUS' | 'SMUS-SPACE-REMOTE-ACCESS' = 'SMAI'): boolean { // Check for SageMaker-specific environment variables first let hasSMEnvVars: boolean = false if (hasSageMakerEnvVars()) { @@ -201,6 +201,9 @@ export function isSageMaker(appName: 'SMAI' | 'SMUS' = 'SMAI'): boolean { return vscode.env.appName === sageMakerAppname && hasSMEnvVars case 'SMUS': return vscode.env.appName === sageMakerAppname && isSageMakerUnifiedStudio() && hasSMEnvVars + case 'SMUS-SPACE-REMOTE-ACCESS': + // When is true, the AWS toolkit is running in remote SSH conenction to SageMaker Unified Studio space + return vscode.env.appName !== sageMakerAppname && isSageMakerUnifiedStudio() && hasSMEnvVars default: return false } diff --git a/packages/core/src/shared/extensions.ts b/packages/core/src/shared/extensions.ts index d9b242e96a6..cd14a9f5996 100644 --- a/packages/core/src/shared/extensions.ts +++ b/packages/core/src/shared/extensions.ts @@ -47,3 +47,5 @@ export interface ExtContext { * Version of the .vsix produced by package.ts with the --debug option. */ export const extensionAlphaVersion = '99.0.0-SNAPSHOT' + +export const cloudformation = 'cloudformation' diff --git a/packages/core/src/shared/extensions/ssh.ts b/packages/core/src/shared/extensions/ssh.ts index 44af313d108..834b945cec7 100644 --- a/packages/core/src/shared/extensions/ssh.ts +++ b/packages/core/src/shared/extensions/ssh.ts @@ -12,7 +12,7 @@ import { ChildProcess, ChildProcessResult } from '../utilities/processUtils' import { ArrayConstructor, NonNullObject } from '../utilities/typeConstructors' import { Settings } from '../settings' import { VSCODE_EXTENSION_ID } from '../extensions' -import { SSM } from 'aws-sdk' +import { StartSessionResponse } from '@aws-sdk/client-ssm' import { ErrorInformation, ToolkitError } from '../errors' const localize = nls.loadMessageBundle() @@ -144,7 +144,7 @@ export async function testSshConnection( hostname: string, sshPath: string, user: string, - session: SSM.StartSessionResponse + session: StartSessionResponse ): Promise { const env = { SESSION_ID: session.SessionId, STREAM_URL: session.StreamUrl, TOKEN: session.TokenValue } const process = new ProcessClass(sshPath, ['-T', `${user}@${hostname}`, 'echo "test connection succeeded" && exit']) diff --git a/packages/core/src/shared/featureConfig.ts b/packages/core/src/shared/featureConfig.ts index c7b111b3243..c0ed174045a 100644 --- a/packages/core/src/shared/featureConfig.ts +++ b/packages/core/src/shared/featureConfig.ts @@ -39,6 +39,8 @@ export const Features = { dataCollectionFeature: 'IDEProjectContextDataCollection', projectContextFeature: 'ProjectContextV2', workspaceContextFeature: 'WorkspaceContext', + preFlareRollbackBIDFeature: 'PreflareRollbackExperiment_BID', + preFlareRollbackIDCFeature: 'PreflareRollbackExperiment_IDC', test: 'testFeature', highlightCommand: 'highlightCommand', } as const @@ -106,6 +108,16 @@ export class FeatureConfigProvider { } } + getPreFlareRollbackGroup(): 'control' | 'treatment' | 'default' { + const variationBid = this.featureConfigs.get(Features.preFlareRollbackBIDFeature)?.variation + const variationIdc = this.featureConfigs.get(Features.preFlareRollbackIDCFeature)?.variation + if (variationBid === 'TREATMENT' || variationIdc === 'TREATMENT') { + return 'treatment' + } else { + return 'control' + } + } + public async listFeatureEvaluations(): Promise { const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const request: ListFeatureEvaluationsRequest = { diff --git a/packages/core/src/shared/fs/templateRegistry.ts b/packages/core/src/shared/fs/templateRegistry.ts index 00afe876c4d..a9ec3a66ac8 100644 --- a/packages/core/src/shared/fs/templateRegistry.ts +++ b/packages/core/src/shared/fs/templateRegistry.ts @@ -18,6 +18,7 @@ import { Timeout } from '../utilities/timeoutUtils' import { localize } from '../utilities/vsCodeUtils' import { PerfLog } from '../logger/perfLogger' import { showMessageWithCancel } from '../utilities/messages' +import { Runtime } from '@aws-sdk/client-lambda' export class CloudFormationTemplateRegistry extends WatchedFiles { public name: string = 'CloudFormationTemplateRegistry' @@ -188,7 +189,7 @@ export function getResourcesForHandlerFromTemplateDatum( resource.Properties, 'Runtime', templateDatum.item - ) + ) as Runtime const registeredCodeUri = CloudFormation.getStringForProperty( resource.Properties, 'CodeUri', diff --git a/packages/core/src/shared/globalState.ts b/packages/core/src/shared/globalState.ts index e8e6a3bff44..9e68836cbf1 100644 --- a/packages/core/src/shared/globalState.ts +++ b/packages/core/src/shared/globalState.ts @@ -8,7 +8,7 @@ import { getLogger } from './logger/logger' import * as redshift from '../awsService/redshift/models/models' import { TypeConstructor, cast } from './utilities/typeConstructors' -type ToolId = 'codecatalyst' | 'codewhisperer' | 'testId' +type ToolId = 'codecatalyst' | 'codewhisperer' | 'testId' | 'smus' export type ToolIdStateKey = `${ToolId}.savedConnectionId` export type JsonSchemasKey = 'devfileSchemaVersion' | 'samAndCfnSchemaVersion' @@ -48,6 +48,7 @@ export type globalKey = | 'aws.toolkit.lsp.versions' | 'aws.toolkit.lsp.manifest' | 'aws.amazonq.customization.overrideV2' + | 'aws.smus.authenticationPreferences' | 'aws.amazonq.regionProfiles' | 'aws.amazonq.regionProfiles.cache' // Deprecated/legacy names. New keys should start with "aws.". @@ -83,6 +84,13 @@ export type globalKey = | 'aws.lambda.remoteDebugSnapshot' // List of Domain-Users to show/hide Sagemaker SpaceApps in AWS Explorer. | 'aws.sagemaker.selectedDomainUsers' + // List of Cluster-Namespaces to show/hide Hyperpod Spaces in AWS Explorer + | 'aws.hyperpod.selectedClusterNamespaces' + // Name of the connection if it's not to the AWS cloud. Current supported value only 'localstack' + | 'aws.toolkit.externalConnection' + | 'aws.cloudformation.region' + | 'aws.cloudformation.selectedResourceTypes' + | 'aws.cloudformation.lsp.manifest' /** * Extension-local (not visible to other vscode extensions) shared state which persists after IDE diff --git a/packages/core/src/shared/logger/logger.ts b/packages/core/src/shared/logger/logger.ts index 95c4c7af769..6a2c4a00511 100644 --- a/packages/core/src/shared/logger/logger.ts +++ b/packages/core/src/shared/logger/logger.ts @@ -15,6 +15,7 @@ export type LogTopic = | 'amazonqWorkspaceLsp' | 'amazonqLsp' | 'amazonqLsp.lspClient' + | 'awsCfnLsp' | 'chat' | 'stepfunctions' | 'unknown' @@ -23,6 +24,7 @@ export type LogTopic = | 'telemetry' | 'proxyUtil' | 'sagemaker' + | 'smus' class ErrorLog { constructor( diff --git a/packages/core/src/shared/lsp/baseLspInstaller.ts b/packages/core/src/shared/lsp/baseLspInstaller.ts index 7acf58ad788..8ba85ebe1e1 100644 --- a/packages/core/src/shared/lsp/baseLspInstaller.ts +++ b/packages/core/src/shared/lsp/baseLspInstaller.ts @@ -26,7 +26,8 @@ export abstract class BaseLspInstaller + loggerName: Extract, + private manifestResolver?: ManifestResolver ) { this.logger = getLogger(loggerName) } @@ -45,7 +46,9 @@ export abstract class BaseLspInstaller fetchResult.res && fetchResult.res.ok && fetchResult.res.body) .flatMap(async (fetchResult) => { const arrBuffer = await fetchResult.res!.arrayBuffer() const data = Buffer.from(arrBuffer) + // Skip hash verification if no hash is provided + if (!fetchResult.hash) { + return [{ filename: fetchResult.filename, data }] + } + const hash = createHash('sha384', data) if (hash === fetchResult.hash) { return [{ filename: fetchResult.filename, data }] } + + logger.error('Invalid hash') return [] }) if (verifyTasks.length !== contents.length) { diff --git a/packages/core/src/shared/lsp/utils/runner.ts b/packages/core/src/shared/lsp/utils/runner.ts index 8116f4dee1b..690ef879480 100644 --- a/packages/core/src/shared/lsp/utils/runner.ts +++ b/packages/core/src/shared/lsp/utils/runner.ts @@ -11,7 +11,7 @@ // Disable because this is a language server. /* eslint-disable aws-toolkits/no-console-log */ -import { CancellationToken, ErrorCodes, ResponseError } from 'vscode-languageserver' +import { CancellationToken, LSPErrorCodes, ResponseError } from 'vscode-languageserver' export function formatError(message: string, err: any): string { if (err instanceof Error) { @@ -90,5 +90,5 @@ export function runSafe( function cancelValue() { console.log('cancelled') - return new ResponseError(ErrorCodes.RequestCancelled, 'Request cancelled') + return new ResponseError(LSPErrorCodes.RequestCancelled, 'Request cancelled') } diff --git a/packages/core/src/shared/sam/activation.ts b/packages/core/src/shared/sam/activation.ts index 855dde39a29..c3fd1376710 100644 --- a/packages/core/src/shared/sam/activation.ts +++ b/packages/core/src/shared/sam/activation.ts @@ -18,8 +18,8 @@ import * as pyLensProvider from '../codelens/pythonCodeLensProvider' import * as goLensProvider from '../codelens/goCodeLensProvider' import { SamTemplateCodeLensProvider } from '../codelens/samTemplateCodeLensProvider' import * as jsLensProvider from '../codelens/typescriptCodeLensProvider' -import { ExtContext, VSCODE_EXTENSION_ID } from '../extensions' -import { getIdeProperties, getIdeType } from '../extensionUtilities' +import { ExtContext } from '../extensions' +import { getIdeProperties } from '../extensionUtilities' import { getLogger } from '../logger/logger' import { PerfLog } from '../logger/perfLogger' import { NoopWatcher } from '../fs/watchedFiles' @@ -28,12 +28,10 @@ import { CodelensRootRegistry } from '../fs/codelensRootRegistry' import { AWS_SAM_DEBUG_TYPE } from './debugger/awsSamDebugConfiguration' import { SamDebugConfigProvider } from './debugger/awsSamDebugger' import { addSamDebugConfiguration } from './debugger/commands/addSamDebugConfiguration' -import { ToolkitPromptSettings } from '../settings' import { shared } from '../utilities/functionUtils' import { SamCliSettings } from './cli/samCliSettings' import { Commands } from '../vscode/commands2' import { runSync } from './sync' -import { showExtensionPage } from '../utilities/vsCodeUtils' import { runDeploy } from './deploy' import { telemetry } from '../telemetry/telemetry' @@ -48,7 +46,6 @@ const supportedLanguages: { */ export async function activate(ctx: ExtContext): Promise { let didActivateCodeLensProviders = false - await createYamlExtensionPrompt() const config = SamCliSettings.instance // Do this "on-demand" because it is slow. @@ -285,152 +282,3 @@ async function activateCodefileOverlays( perflog.done() return disposables } - -/** - * Creates a prompt (via toast) to guide users to installing the Red Hat YAML extension. - * This is necessary for displaying codelenses on templaye YAML files. - * Will show once per extension activation at most (all prompting triggers are disposed of on first trigger) - * Will not show if the YAML extension is installed or if a user has permanently dismissed the message. - */ -async function createYamlExtensionPrompt(): Promise { - const settings = ToolkitPromptSettings.instance - - /** - * Prompt the user to install the YAML plugin when AWSTemplateFormatVersion becomes available as a top level key - * in the document - * @param event An vscode text document change event - * @returns nothing - */ - async function promptOnAWSTemplateFormatVersion( - event: vscode.TextDocumentChangeEvent, - yamlPromptDisposables: vscode.Disposable[] - ): Promise { - for (const change of event.contentChanges) { - const changedLine = event.document.lineAt(change.range.start.line) - if (changedLine.text.includes('AWSTemplateFormatVersion')) { - await promptInstallYamlPlugin(yamlPromptDisposables) - return - } - } - return - } - - // Show this only in VSCode since other VSCode-like IDEs (e.g. Theia) may - // not have a marketplace or contain the YAML plugin. - if ( - settings.isPromptEnabled('yamlExtPrompt') && - getIdeType() === 'vscode' && - !vscode.extensions.getExtension(VSCODE_EXTENSION_ID.yaml) - ) { - // Disposed immediately after showing one, so the user isn't prompted - // more than once per session. - const yamlPromptDisposables: vscode.Disposable[] = [] - - // user opens a template file - vscode.workspace.onDidOpenTextDocument( - async (doc: vscode.TextDocument) => { - void promptInstallYamlPluginFromFilename(doc.fileName, yamlPromptDisposables) - }, - undefined, - yamlPromptDisposables - ) - - // user swaps to an already-open template file that didn't have focus - vscode.window.onDidChangeActiveTextEditor( - async (editor: vscode.TextEditor | undefined) => { - await promptInstallYamlPluginFromEditor(editor, yamlPromptDisposables) - }, - undefined, - yamlPromptDisposables - ) - - const promptNotifications = new Map>() - vscode.workspace.onDidChangeTextDocument( - (event: vscode.TextDocumentChangeEvent) => { - const uri = event.document.uri.toString() - if ( - event.document.languageId === 'yaml' && - !vscode.extensions.getExtension(VSCODE_EXTENSION_ID.yaml) && - !promptNotifications.has(uri) - ) { - promptNotifications.set( - uri, - promptOnAWSTemplateFormatVersion(event, yamlPromptDisposables).finally(() => - promptNotifications.delete(uri) - ) - ) - } - }, - undefined, - yamlPromptDisposables - ) - - vscode.workspace.onDidCloseTextDocument((event: vscode.TextDocument) => { - promptNotifications.delete(event.uri.toString()) - }) - - // user already has an open template with focus - // prescreen if a template.yaml is current open so we only call once - const openTemplateYamls = vscode.window.visibleTextEditors.filter((editor) => { - const fileName = editor.document.fileName - return fileName.endsWith('template.yaml') || fileName.endsWith('template.yml') - }) - - if (openTemplateYamls.length > 0) { - void promptInstallYamlPluginFromEditor(openTemplateYamls[0], yamlPromptDisposables) - } - } -} - -async function promptInstallYamlPluginFromEditor( - editor: vscode.TextEditor | undefined, - disposables: vscode.Disposable[] -): Promise { - if (editor) { - void promptInstallYamlPluginFromFilename(editor.document.fileName, disposables) - } -} - -/** - * Prompt user to install YAML plugin for template.yaml and template.yml files - * @param fileName File name to check against - * @param disposables List of disposables to dispose of when the filename is a template YAML file - */ -async function promptInstallYamlPluginFromFilename(fileName: string, disposables: vscode.Disposable[]): Promise { - if (fileName.endsWith('template.yaml') || fileName.endsWith('template.yml')) { - void promptInstallYamlPlugin(disposables) - } -} - -/** - * Show the install YAML extension prompt and dispose other listeners - * @param disposables - */ -async function promptInstallYamlPlugin(disposables: vscode.Disposable[]) { - // immediately dispose other triggers so it doesn't flash again - for (const prompt of disposables) { - prompt.dispose() - } - const settings = ToolkitPromptSettings.instance - - const installBtn = localize('AWS.missingExtension.install', 'Install...') - const permanentlySuppress = localize('AWS.message.info.yaml.suppressPrompt', "Don't show again") - - const response = await vscode.window.showInformationMessage( - localize( - 'AWS.message.info.yaml.prompt', - 'Install YAML extension for more {0} features in CloudFormation templates', - getIdeProperties().company - ), - installBtn, - permanentlySuppress - ) - - switch (response) { - case installBtn: - await showExtensionPage(VSCODE_EXTENSION_ID.yaml) - break - case permanentlySuppress: - await settings.disablePrompt('yamlExtPrompt') - } -} diff --git a/packages/core/src/shared/sam/cli/samCliFeatureRegistry.ts b/packages/core/src/shared/sam/cli/samCliFeatureRegistry.ts new file mode 100644 index 00000000000..d491157388f --- /dev/null +++ b/packages/core/src/shared/sam/cli/samCliFeatureRegistry.ts @@ -0,0 +1,179 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as semver from 'semver' +import * as CloudFormation from '../../../shared/cloudformation/cloudformation' +import { getSamCliPathAndVersion } from '../utils' +import { getLogger } from '../../logger/logger' +import { openUrl } from '../../utilities/vsCodeUtils' +import { awsClis } from '../../utilities/cliUtils' +import { ToolkitError } from '../../errors' + +/** + * Registry of SAM CLI features and their minimum required versions. + * This allows us to validate templates before invoking SAM CLI commands + * and provide clear error messages when features are not supported given + * current SAM CLI version. + */ + +/** + * Detection rule for identifying features in templates. + * Each rule specifies how to detect a feature in the template structure. + */ +export interface FeatureDetectionRule { + /** Check if a resource matches this feature */ + matchResource?: (resourceType: string, properties: any) => boolean + /** Check if globals section matches this feature */ + matchGlobals?: (globals: any) => boolean +} + +export interface SamCliFeature { + /** Unique identifier for the feature */ + id: string + /** Human-readable name */ + name: string + /** Minimum SAM CLI version required */ + minVersion: string + /** Description of the feature */ + description: string + /** Detection rules for this feature */ + detectionRule: FeatureDetectionRule +} + +/** + * Registry of SAM CLI features and their version requirements. + * Add new features here as they are introduced in SAM CLI. + * + * To add a new feature: + * 1. Add a new entry to this registry with detection rules + * 2. The detection logic will automatically pick it up + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const SAM_CLI_FEATURE_REGISTRY: Record = { + CAPACITY_PROVIDER: { + id: 'CAPACITY_PROVIDER', + name: '`CapacityProvider`', + minVersion: '1.149.0', + description: 'AWS::Serverless::CapacityProvider resource', + detectionRule: { + matchResource: (resourceType: string) => resourceType === 'AWS::Serverless::CapacityProvider', + }, + }, + CAPACITY_PROVIDER_CONFIG: { + id: 'CAPACITY_PROVIDER_CONFIG', + name: 'CapacityProviderConfig', + minVersion: '1.149.0', + description: 'AWS::Serverless::Function CapacityProviderConfig property', + detectionRule: { + matchResource: (resourceType: string, properties: any) => + resourceType === 'AWS::Serverless::Function' && !!properties?.CapacityProviderConfig, + matchGlobals: (globals: any) => !!globals.Function?.CapacityProviderConfig, + }, + }, +} as const + +/** + * Detects SAM CLI features used in a CloudFormation template. + * This function is completely data-driven - it iterates through all features + * in the registry and applies their detection rules. + * + * Logic: + * - Automatically fetches SAM CLI version (SAM CLI must be available) + * - Filters features to only those unsupported by current version before searching + * - Returns early if no unsupported features exist + * + * @param template The parsed CloudFormation template + * @param samCliVersion Optional SAM CLI version (fetched automatically if not provided) + * @returns Object containing detected unsupported features and the SAM CLI version used + */ +export async function detectFeaturesInTemplate( + template: any, + samCliVersion?: string +): Promise<{ unsupported: SamCliFeature[]; version: string }> { + const allFeatures = Object.values(SAM_CLI_FEATURE_REGISTRY) + + // Fetch SAM CLI version if not provided + const version = samCliVersion ?? String((await getSamCliPathAndVersion()).parsedVersion ?? '0.0.0') + + // Step 1: Filter to only features that require a version greater than current + const unsupportedFeatures = allFeatures.filter((feature) => semver.gt(feature.minVersion, version)) + + // Step 2: Early exit if no unsupported features exist + if (unsupportedFeatures.length === 0) { + return { unsupported: [], version } + } + + // Step 3: Search template for only the unsupported features + const detected: SamCliFeature[] = [] + const resources = template?.Resources ?? {} + const globals = template?.Globals ?? {} + + for (const feature of unsupportedFeatures) { + let found = false + + // Check resources if matchResource rule exists + if (feature.detectionRule.matchResource && !found) { + for (const resource of Object.values(resources) as any[]) { + if (feature.detectionRule.matchResource(resource.Type, resource.Properties)) { + detected.push(feature) + found = true + break + } + } + } + + // Check globals if matchGlobals rule exists and not yet found + if (feature.detectionRule.matchGlobals && !found) { + if (feature.detectionRule.matchGlobals(globals)) { + detected.push(feature) + } + } + } + + return { unsupported: detected, version } +} + +/** + * Validates a template file against the current SAM CLI version. + * Shows a user prompt and throws an error if the template contains unsupported features. + * + * @param templateUri URI to the CloudFormation template file + * @throws Error if template contains features unsupported by current SAM CLI version + */ +export async function validateSamCliVersionForTemplateFile(templateUri: vscode.Uri): Promise { + const samTemplate = await CloudFormation.tryLoad(templateUri) + if (!samTemplate.template) { + return // Template couldn't be loaded, skip validation + } + + const { unsupported, version } = await detectFeaturesInTemplate(samTemplate.template) + + if (unsupported.length === 0) { + return // All features are supported + } + + // Calculate required version + const requiredVersion = unsupported.reduce( + (max, feature) => (semver.gt(feature.minVersion, max) ? feature.minVersion : max), + unsupported[0].minVersion + ) + + const featureList = unsupported.map((f) => `${f.description} (requires ${f.minVersion})`).join(' ') + const errorMessage = `Your SAM CLI version (${version}) does not support the following features: ${featureList}. Please upgrade to SAM CLI version ${requiredVersion} or higher.` + + getLogger().warn(`SAM CLI version check failed: ${errorMessage}`) + throw new ToolkitError(errorMessage) +} + +export async function showWarningWithSamCliUpdateInstruction(errorMessage: string): Promise { + // Show user prompt with option to view update instructions + const updateInstruction = 'View SAM CLI Update Instructions' + const selection = await vscode.window.showWarningMessage(errorMessage, updateInstruction) + + if (selection === updateInstruction) { + void openUrl(vscode.Uri.parse(awsClis['sam-cli'].manualInstallLink)) + } +} diff --git a/packages/core/src/shared/sam/cli/samCliInit.ts b/packages/core/src/shared/sam/cli/samCliInit.ts index b087b7cc5f4..366c7539519 100644 --- a/packages/core/src/shared/sam/cli/samCliInit.ts +++ b/packages/core/src/shared/sam/cli/samCliInit.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Runtime } from 'aws-sdk/clients/lambda' +import { Runtime } from '@aws-sdk/client-lambda' import { SchemaTemplateExtraContext } from '../../../eventSchemas/templates/schemasAppTemplateUtils' import { Architecture, DependencyManager } from '../../../lambda/models/samLambdaRuntime' import { getSamCliTemplateParameter, SamTemplate } from '../../../lambda/models/samTemplates' diff --git a/packages/core/src/shared/sam/cli/samCliListResources.ts b/packages/core/src/shared/sam/cli/samCliListResources.ts index 341a48c30d6..ed1eb9115da 100644 --- a/packages/core/src/shared/sam/cli/samCliListResources.ts +++ b/packages/core/src/shared/sam/cli/samCliListResources.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode' import { logAndThrowIfUnexpectedExitCode, SamCliProcessInvoker } from './samCliInvokerUtils' import { getSpawnEnv } from '../../env/resolveEnv' import { getLogger } from '../../logger/logger' +import { showWarningWithSamCliUpdateInstruction, validateSamCliVersionForTemplateFile } from './samCliFeatureRegistry' export interface SamCliListResourcesParameters { templateFile: string @@ -19,6 +20,18 @@ export async function runSamCliListResource( listStackResourcesArguments: SamCliListResourcesParameters, invoker: SamCliProcessInvoker ): Promise { + // Validate template features before invoking SAM CLI + try { + const templateUri = vscode.Uri.file(listStackResourcesArguments.templateFile) + await validateSamCliVersionForTemplateFile(templateUri) + } catch (validationError: any) { + // Validation failed, show error with update instructions + getLogger().warn('SAM CLI feature validation failed: %O', validationError) + const errorMessage = `Failed to run SAM CLI list resources. ${validationError.message}` + void showWarningWithSamCliUpdateInstruction(errorMessage) + return [] + } + const args = [ 'list', 'resources', diff --git a/packages/core/src/shared/sam/cli/samCliLocalInvoke.ts b/packages/core/src/shared/sam/cli/samCliLocalInvoke.ts index 265b0c86338..c1e60ebcb41 100644 --- a/packages/core/src/shared/sam/cli/samCliLocalInvoke.ts +++ b/packages/core/src/shared/sam/cli/samCliLocalInvoke.ts @@ -15,7 +15,7 @@ import globals from '../../extensionGlobals' import { SamCliSettings } from './samCliSettings' import { addTelemetryEnvVar, collectSamErrors, SamCliError } from './samCliInvokerUtils' import { fs } from '../../fs/fs' -import { Runtime } from 'aws-sdk/clients/lambda' +import { Runtime } from '@aws-sdk/client-lambda' import { getSamCliPathAndVersion } from '../utils' import { deprecatedRuntimes } from '../../../lambda/models/samLambdaRuntime' diff --git a/packages/core/src/shared/sam/cli/samCliRemoteTestEvent.ts b/packages/core/src/shared/sam/cli/samCliRemoteTestEvent.ts index 7c3d79ca9f2..e17a5d49c9e 100644 --- a/packages/core/src/shared/sam/cli/samCliRemoteTestEvent.ts +++ b/packages/core/src/shared/sam/cli/samCliRemoteTestEvent.ts @@ -24,6 +24,7 @@ export interface SamCliRemoteTestEventsParameters { projectRoot?: vscode.Uri stackName?: string logicalId?: string + force?: boolean } export async function runSamCliRemoteTestEvents( @@ -51,8 +52,14 @@ export async function runSamCliRemoteTestEvents( if (remoteTestEventsParameters.operation === TestEventsOperation.Put && remoteTestEventsParameters.eventSample) { const tempFileUri = vscode.Uri.file(path.join(os.tmpdir(), 'event-sample.json')) - await vscode.workspace.fs.writeFile(tempFileUri, Buffer.from(remoteTestEventsParameters.eventSample, 'utf8')) + const encoder = new TextEncoder() + await vscode.workspace.fs.writeFile(tempFileUri, encoder.encode(remoteTestEventsParameters.eventSample)) args.push('--file', tempFileUri.fsPath) + + // Add --force flag when updating existing events + if (remoteTestEventsParameters.force) { + args.push('--force') + } } const childProcessResult = await invoker.invoke({ diff --git a/packages/core/src/shared/sam/debugger/awsSamDebugConfiguration.ts b/packages/core/src/shared/sam/debugger/awsSamDebugConfiguration.ts index 389b91f6208..f11be86d00c 100644 --- a/packages/core/src/shared/sam/debugger/awsSamDebugConfiguration.ts +++ b/packages/core/src/shared/sam/debugger/awsSamDebugConfiguration.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode' import * as path from 'path' -import { Runtime } from 'aws-sdk/clients/lambda' +import { Runtime } from '@aws-sdk/client-lambda' import { getNormalizedRelativePath } from '../../utilities/pathUtils' import { APIGatewayProperties, diff --git a/packages/core/src/shared/sam/debugger/awsSamDebugConfigurationValidator.ts b/packages/core/src/shared/sam/debugger/awsSamDebugConfigurationValidator.ts index f71310f0261..b400746d7e6 100644 --- a/packages/core/src/shared/sam/debugger/awsSamDebugConfigurationValidator.ts +++ b/packages/core/src/shared/sam/debugger/awsSamDebugConfigurationValidator.ts @@ -20,6 +20,7 @@ import { } from './awsSamDebugConfiguration' import { tryGetAbsolutePath } from '../../utilities/workspaceUtils' import { CloudFormationTemplateRegistry } from '../../fs/templateRegistry' +import { Runtime } from '@aws-sdk/client-lambda' export interface ValidationResult { isValid: boolean @@ -187,7 +188,7 @@ export class DefaultAwsSamDebugConfigurationValidator implements AwsSamDebugConf } } // can't infer the runtime for image-based lambdas - if (!config.lambda?.runtime || !samImageLambdaRuntimes().has(config.lambda.runtime)) { + if (!config.lambda?.runtime || !samImageLambdaRuntimes().has(config.lambda.runtime as Runtime)) { return { isValid: false, message: localize( @@ -201,7 +202,7 @@ export class DefaultAwsSamDebugConfigurationValidator implements AwsSamDebugConf // TODO: Decide what to do with this re: refs. // As of now, this has to be directly declared without a ref, despite the fact that SAM will handle a ref. // Should we just pass validation off to SAM and ignore validation at this point, or should we directly process the value (like the handler)? - const runtime = CloudFormation.getStringForProperty(resource?.Properties, 'Runtime', cfnTemplate) + const runtime = CloudFormation.getStringForProperty(resource?.Properties, 'Runtime', cfnTemplate) as Runtime if (!runtime || !samZipLambdaRuntimes.has(runtime)) { return { isValid: false, @@ -262,7 +263,10 @@ export class DefaultAwsSamDebugConfigurationValidator implements AwsSamDebugConf } private validateCodeConfig(debugConfiguration: AwsSamDebuggerConfiguration): ValidationResult { - if (!debugConfiguration.lambda?.runtime || !samZipLambdaRuntimes.has(debugConfiguration.lambda.runtime)) { + if ( + !debugConfiguration.lambda?.runtime || + !samZipLambdaRuntimes.has(debugConfiguration.lambda.runtime as Runtime) + ) { return { isValid: false, message: localize( diff --git a/packages/core/src/shared/sam/debugger/awsSamDebugger.ts b/packages/core/src/shared/sam/debugger/awsSamDebugger.ts index 2b6e9311e6b..217ce451dcd 100644 --- a/packages/core/src/shared/sam/debugger/awsSamDebugger.ts +++ b/packages/core/src/shared/sam/debugger/awsSamDebugger.ts @@ -59,7 +59,8 @@ import { minSamCliVersionForImageSupport, minSamCliVersionForGoSupport } from '. import { getIdeProperties } from '../../extensionUtilities' import { resolve } from 'path' import globals from '../../extensionGlobals' -import { Runtime, telemetry } from '../../telemetry/telemetry' +import { telemetry, Runtime as TelemetryRuntime } from '../../telemetry/telemetry' +import { Runtime } from '@aws-sdk/client-lambda' import { ErrorInformation, isUserCancelledError, ToolkitError } from '../../errors' import { openLaunchJsonFile } from './commands/addSamDebugConfiguration' import { Logging } from '../../logger/commands' @@ -471,8 +472,8 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider } const isZip = CloudFormation.isZipLambdaResource(templateResource?.Properties) - const runtime: string | undefined = - config.lambda?.runtime ?? + const runtime: Runtime | undefined = + (config.lambda?.runtime as Runtime) ?? (template && isZip ? CloudFormation.getStringForProperty(templateResource?.Properties, 'Runtime', template) : undefined) ?? @@ -690,7 +691,7 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider public async invokeConfig(config: SamLaunchRequestArgs): Promise { telemetry.record({ debug: !config.noDebug, - runtime: config.runtime as Runtime, + runtime: config.runtime as TelemetryRuntime, lambdaArchitecture: config.architecture, lambdaPackageType: (await isImageLambdaConfig(config)) ? 'Image' : 'Zip', version: await getSamCliVersion(getSamCliContext()), diff --git a/packages/core/src/shared/sam/debugger/javaSamDebug.ts b/packages/core/src/shared/sam/debugger/javaSamDebug.ts index 26ebad889d6..9078c9e2a03 100644 --- a/packages/core/src/shared/sam/debugger/javaSamDebug.ts +++ b/packages/core/src/shared/sam/debugger/javaSamDebug.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { Runtime } from '@aws-sdk/client-lambda' import { getCodeRoot, isImageLambdaConfig } from '../../../lambda/local/debugConfiguration' import { RuntimeFamily } from '../../../lambda/models/samLambdaRuntime' import { ExtContext } from '../../extensions' @@ -55,9 +56,8 @@ function getJavaOptionsEnvVar(config: SamLaunchRequestArgs): string { // https://github.com/aws/aws-sam-cli/blob/86f88cbd7df365960f7015c5d086b0db7aedd9d5/samcli/local/docker/lambda_debug_settings.py#L53 return `-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=*:${config.debugPort} -XX:MaxHeapSize=2834432k -XX:MaxMetaspaceSize=163840k -XX:ReservedCodeCacheSize=81920k -XX:+UseSerialGC -XX:-TieredCompilation -Djava.net.preferIPv4Stack=true` case 'java17': - // https://github.com/aws/aws-sam-cli/blob/90aa5cf11e1c5cbfbe66aea2e2de10d478d48231/samcli/local/docker/lambda_debug_settings.py#L86 - return `-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=*:${config.debugPort} -XX:MaxHeapSize=2834432k -XX:+UseSerialGC -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Djava.net.preferIPv4Stack=true` case 'java21': + case 'java25' as Runtime: // https://github.com/aws/aws-sam-cli/blob/90aa5cf11e1c5cbfbe66aea2e2de10d478d48231/samcli/local/docker/lambda_debug_settings.py#L96 return `-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=*:${config.debugPort} -XX:MaxHeapSize=2834432k -XX:+UseSerialGC -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Djava.net.preferIPv4Stack=true` default: diff --git a/packages/core/src/shared/sam/debugger/pythonSamDebug.ts b/packages/core/src/shared/sam/debugger/pythonSamDebug.ts index 670c2ddd71a..12f25d79810 100644 --- a/packages/core/src/shared/sam/debugger/pythonSamDebug.ts +++ b/packages/core/src/shared/sam/debugger/pythonSamDebug.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Runtime } from 'aws-sdk/clients/lambda' +import { Runtime } from '@aws-sdk/client-lambda' import * as os from 'os' import * as path from 'path' import { @@ -154,20 +154,13 @@ function getPythonExeAndBootstrap(runtime: Runtime) { // unfortunately new 'Image'-base images did not standardize the paths // https://github.com/aws/aws-sam-cli/blob/7d5101a8edeb575b6925f9adecf28f47793c403c/samcli/local/docker/lambda_debug_settings.py switch (runtime) { - case 'python3.7': - return { python: '/var/lang/bin/python3.7', bootstrap: '/var/runtime/bootstrap' } - case 'python3.8': - return { python: '/var/lang/bin/python3.8', bootstrap: '/var/runtime/bootstrap.py' } case 'python3.9': - return { python: '/var/lang/bin/python3.9', bootstrap: '/var/runtime/bootstrap.py' } case 'python3.10': - return { python: '/var/lang/bin/python3.10', bootstrap: '/var/runtime/bootstrap.py' } case 'python3.11': - return { python: '/var/lang/bin/python3.11', bootstrap: '/var/runtime/bootstrap.py' } case 'python3.12': - return { python: '/var/lang/bin/python3.12', bootstrap: '/var/runtime/bootstrap.py' } - case 'python3.13': - return { python: '/var/lang/bin/python3.13', bootstrap: '/var/runtime/bootstrap.py' } + case 'python3.13' as Runtime: + case 'python3.14' as Runtime: + return { python: `/var/lang/bin/${runtime}`, bootstrap: '/var/runtime/bootstrap.py' } default: throw new Error(`Python SAM debug logic ran for invalid Python runtime: ${runtime}`) } diff --git a/packages/core/src/shared/sam/localLambdaRunner.ts b/packages/core/src/shared/sam/localLambdaRunner.ts index 6d28048be48..447fad81f26 100644 --- a/packages/core/src/shared/sam/localLambdaRunner.ts +++ b/packages/core/src/shared/sam/localLambdaRunner.ts @@ -32,6 +32,7 @@ import { SamCliError } from './cli/samCliInvokerUtils' import fs from '../fs/fs' import { getSpawnEnv } from '../env/resolveEnv' import { asEnvironmentVariables } from '../../auth/credentials/utils' +import { Runtime } from '@aws-sdk/client-lambda' const localize = nls.loadMessageBundle() @@ -247,7 +248,7 @@ async function invokeLambdaHandler( parameterOverrides: config.parameterOverrides, name: config.name, region: config.region, - runtime: config.lambda?.runtime, + runtime: config.lambda?.runtime as Runtime, } // sam local invoke ... @@ -524,7 +525,7 @@ export async function waitForPort(port: number, timeout: Timeout, isDebugPort: b } } -export function shouldAppendRelativePathToFuncHandler(runtime: string): boolean { +export function shouldAppendRelativePathToFuncHandler(runtime: Runtime): boolean { // getFamily will throw an error if the runtime doesn't exist switch (getFamily(runtime)) { case RuntimeFamily.NodeJS: diff --git a/packages/core/src/shared/sam/utils.ts b/packages/core/src/shared/sam/utils.ts index ca2446fe3e9..51e93e80645 100644 --- a/packages/core/src/shared/sam/utils.ts +++ b/packages/core/src/shared/sam/utils.ts @@ -18,6 +18,7 @@ import { telemetry } from '../telemetry/telemetry' import globals from '../extensionGlobals' import { getLogger } from '../logger/logger' import { ChildProcessResult } from '../utilities/processUtils' +import { Runtime } from '@aws-sdk/client-lambda' /** * @description determines the root directory of the project given Template Item @@ -66,7 +67,7 @@ export async function isDotnetRuntime(templateUri: vscode.Uri, contents?: string } } } - const globalRuntime = samTemplate.template.Globals?.Function?.Runtime as string + const globalRuntime = samTemplate.template.Globals?.Function?.Runtime as Runtime return globalRuntime ? getFamily(globalRuntime) === RuntimeFamily.DotNet : false } diff --git a/packages/core/src/shared/schemas.ts b/packages/core/src/shared/schemas.ts index 1506908a7c8..8e737d9ce67 100644 --- a/packages/core/src/shared/schemas.ts +++ b/packages/core/src/shared/schemas.ts @@ -149,37 +149,8 @@ export async function getDefaultSchemas(): Promise { const devfileSchemaUri = GlobalStorage.devfileSchemaUri() const devfileSchemaVersion = await getPropertyFromJsonUrl(devfileManifestUrl, 'tag_name') - // Sam schema is a superset of Cfn schema, so we can use it for both - const samAndCfnSchemaDestinationUri = GlobalStorage.samAndCfnSchemaDestinationUri() - const schemas: Schemas = {} - try { - await updateSchemaFromRemoteETag({ - destination: samAndCfnSchemaDestinationUri, - eTag: undefined, - url: samAndCfnSchemaUrl, - cacheKey: 'samAndCfnSchemaVersion', - title: schemaPrefix + 'cloudformation.schema.json', - }) - schemas['cfn'] = samAndCfnSchemaDestinationUri - } catch (e) { - getLogger().verbose('Could not download sam/cfn schema: %s', (e as Error).message) - } - - try { - await updateSchemaFromRemoteETag({ - destination: samAndCfnSchemaDestinationUri, - eTag: undefined, - url: samAndCfnSchemaUrl, - cacheKey: 'samAndCfnSchemaVersion', - title: schemaPrefix + 'sam.schema.json', - }) - schemas['sam'] = samAndCfnSchemaDestinationUri - } catch (e) { - getLogger().verbose('Could not download sam/cfn schema: %s', (e as Error).message) - } - try { await updateSchemaFromRemote({ destination: devfileSchemaUri, diff --git a/packages/core/src/shared/settings-amazonq.gen.ts b/packages/core/src/shared/settings-amazonq.gen.ts index 637c5b1b12e..2ca8481b55e 100644 --- a/packages/core/src/shared/settings-amazonq.gen.ts +++ b/packages/core/src/shared/settings-amazonq.gen.ts @@ -36,7 +36,9 @@ export const amazonqSettings = { "amazonQ.workspaceIndexMaxFileSize": {}, "amazonQ.workspaceIndexCacheDirPath": {}, "amazonQ.workspaceIndexIgnoreFilePatterns": {}, - "amazonQ.ignoredSecurityIssues": {} + "amazonQ.ignoredSecurityIssues": {}, + "amazonQ.proxy.certificateAuthority": {}, + "amazonQ.proxy.enableProxyAndCertificateAutoDiscovery": {} } export default amazonqSettings diff --git a/packages/core/src/shared/settings-toolkit.gen.ts b/packages/core/src/shared/settings-toolkit.gen.ts index 55bc77f9828..04bb4f86d22 100644 --- a/packages/core/src/shared/settings-toolkit.gen.ts +++ b/packages/core/src/shared/settings-toolkit.gen.ts @@ -42,6 +42,7 @@ export const toolkitSettings = { }, "aws.experiments": { "jsonResourceModification": {}, + "cloudFormationService": {}, "amazonqLSP": {}, "amazonqLSPInline": {}, "amazonqChatLSP": {}, @@ -53,7 +54,32 @@ export const toolkitSettings = { "aws.accessAnalyzer.policyChecks.checkNoNewAccessFilePath": {}, "aws.accessAnalyzer.policyChecks.checkAccessNotGrantedFilePath": {}, "aws.accessAnalyzer.policyChecks.cloudFormationParameterFilePath": {}, - "aws.sagemaker.studio.spaces.enableIdentityFiltering": {} + "aws.sagemaker.studio.spaces.enableIdentityFiltering": {}, + "aws.cloudformation.telemetry.enabled": {}, + "aws.cloudformation.hover.enabled": {}, + "aws.cloudformation.completion.enabled": {}, + "aws.cloudformation.diagnostics.cfnLint.enabled": {}, + "aws.cloudformation.diagnostics.cfnLint.lintOnChange": {}, + "aws.cloudformation.diagnostics.cfnLint.delayMs": {}, + "aws.cloudformation.diagnostics.cfnLint.path": {}, + "aws.cloudformation.diagnostics.cfnLint.customization": { + "ignoreChecks": {}, + "includeChecks": {}, + "mandatoryChecks": {}, + "includeExperimental": {}, + "configureRules": {}, + "regions": {}, + "customRules": {}, + "appendRules": {}, + "overrideSpec": {}, + "registrySchemas": {} + }, + "aws.cloudformation.diagnostics.cfnGuard.enabled": {}, + "aws.cloudformation.diagnostics.cfnGuard.validateOnChange": {}, + "aws.cloudformation.diagnostics.cfnGuard.enabledRulePacks": {}, + "aws.cloudformation.diagnostics.cfnGuard.rulesFile": {}, + "aws.cloudformation.s3": {}, + "aws.cloudformation.environment.saveOptions": {} } export default toolkitSettings diff --git a/packages/core/src/shared/settings.ts b/packages/core/src/shared/settings.ts index 4e3e99f8207..20bce5f21ea 100644 --- a/packages/core/src/shared/settings.ts +++ b/packages/core/src/shared/settings.ts @@ -778,10 +778,12 @@ const devSettings = { codewhispererService: Record(String, String), amazonqLsp: Record(String, String), amazonqWorkspaceLsp: Record(String, String), + cloudformationLsp: Record(String, String), ssoCacheDirectory: String, autofillStartUrl: String, webAuth: Boolean, notificationsPollInterval: Number, + datazoneScope: String, } type ResolvedDevSettings = FromDescriptor type AwsDevSetting = keyof ResolvedDevSettings @@ -792,6 +794,7 @@ interface ServiceTypeMap { amazonqLsp: object // type is provided inside of amazon q amazonqWorkspaceLsp: object // type is provided inside of amazon q codewhispererService: CodeWhispererConfig + cloudformationLsp: object // type is provided inside of cloudformation lsp } /** diff --git a/packages/core/src/shared/sshConfig.ts b/packages/core/src/shared/sshConfig.ts index bba23b9a4d8..3b916f65432 100644 --- a/packages/core/src/shared/sshConfig.ts +++ b/packages/core/src/shared/sshConfig.ts @@ -85,7 +85,20 @@ export class SshConfig { protected async matchSshSection() { const result = await this.checkSshOnHost() if (result.exitCode !== 0) { - return Result.err(result.error ?? new Error(`ssh check against host failed: ${result.exitCode}`)) + // Format stderr error message for display to user + let errorMessage = result.stderr?.trim() || `ssh check against host failed: ${result.exitCode}` + const sshConfigPath = getSshConfigPath() + // Remove the SSH config file path prefix from error messages to make them more readable + // SSH errors often include the full path like "/Users/name/.ssh/config: line 5: Bad configuration option" + errorMessage = errorMessage.replace(new RegExp(`${sshConfigPath}:? `, 'g'), '').trim() + + if (result.error) { + // System level error + return Result.err(ToolkitError.chain(result.error, errorMessage)) + } + + // SSH ran but returned error exit code + return Result.err(new ToolkitError(errorMessage, { code: 'SshCheckFailed' })) } const matches = result.stdout.match(this.proxyCommandRegExp) return Result.ok(matches?.[0]) @@ -208,7 +221,7 @@ Host ${this.configHostName} protected createSSHConfigSection(proxyCommand: string): string { if (this.scriptPrefix === 'sagemaker_connect') { - return `${this.getSageMakerSSHConfig(proxyCommand)}User '%r'\n` + return `${this.getSageMakerSSHConfig(proxyCommand)}` } else if (this.keyPath) { return `${this.getBaseSSHConfig(proxyCommand)}IdentityFile '${this.keyPath}'\n User '%r'\n` } diff --git a/packages/core/src/shared/telemetry/util.ts b/packages/core/src/shared/telemetry/util.ts index e6c7254e878..d9c85d07c1b 100644 --- a/packages/core/src/shared/telemetry/util.ts +++ b/packages/core/src/shared/telemetry/util.ts @@ -31,6 +31,7 @@ import { telemetry } from './telemetry' import { v5 as uuidV5 } from 'uuid' import { ToolkitError } from '../errors' import { GlobalState } from '../globalState' +import type { UserAgent } from '@aws-sdk/types' const legacySettingsTelemetryValueDisable = 'Disable' const legacySettingsTelemetryValueEnable = 'Enable' @@ -240,20 +241,46 @@ export function getUserAgent( opt?: { includePlatform?: boolean; includeClientId?: boolean }, globalState = globals.globalState ): string { - const pairs = isAmazonQ() - ? [`AmazonQ-For-VSCode/${extensionVersion}`] - : [`AWS-Toolkit-For-VSCode/${extensionVersion}`] + return userAgentPairsToString(getUserAgentPairs(opt, globalState)) +} + +/** + * Returns a UserAgent array (AWS SDK v3 format) with proper [name, version] pairs. + * + * Omits the platform and `ClientId` pairs by default. + * + * @returns Array of [name, version] tuples for AWS SDK v3's customUserAgent option + */ +export function getUserAgentPairs( + opt?: { includePlatform?: boolean; includeClientId?: boolean }, + globalState = globals.globalState +): UserAgent { + const pairs: UserAgent = isAmazonQ() + ? [['AmazonQ-For-VSCode', extensionVersion]] + : [['AWS-Toolkit-For-VSCode', extensionVersion]] if (opt?.includePlatform) { - pairs.push(platformPair()) + const platform = platformPair() + const [name, version] = platform.split('/') + if (name && version) { + pairs.push([name, version]) + } } if (opt?.includeClientId) { const clientId = getClientId(globalState) - pairs.push(`ClientId/${clientId}`) + pairs.push(['ClientId', clientId]) } - return pairs.join(' ') + return pairs +} + +/** + * Converts UserAgent array format to traditional user agent string format. + * Example: [['LAMBDA-DEBUG', '1.0.0'], ['AWS-Toolkit', '2.0']] => "LAMBDA-DEBUG/1.0.0 AWS-Toolkit/2.0" + */ +export function userAgentPairsToString(pairs: UserAgent): string { + return pairs.map(([name, version]) => `${name}/${version}`).join(' ') } /** diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json index 1128eef8ab6..fed66c6438a 100644 --- a/packages/core/src/shared/telemetry/vscodeTelemetry.json +++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json @@ -1,5 +1,19 @@ { "types": [ + { + "name": "toolId", + "type": "string", + "description": "The tool being installed", + "allowedValues": [ + "session-manager-plugin", + "dotnet-lambda-deploy", + "dotnet-deploy-cli", + "aws-cli", + "sam-cli", + "docker", + "finch" + ] + }, { "name": "amazonQProfileRegion", "type": "string", @@ -238,6 +252,79 @@ "name": "executedCount", "type": "int", "description": "The number of executed operations" + }, + { + "name": "amazonqAutoDebugCommandType", + "type": "string", + "allowedValues": ["fixWithQ", "fixAllWithQ", "explainProblem"], + "description": "The type of auto debug command executed" + }, + { + "name": "amazonqAutoDebugAction", + "type": "string", + "allowedValues": ["invoked", "completed"], + "description": "The action performed (invoked or completed)" + }, + { + "name": "amazonqAutoDebugProblemCount", + "type": "int", + "description": "Number of problems being processed" + }, + { + "name": "smusDomainId", + "type": "string", + "description": "SMUS domain identifier" + }, + { + "name": "smusProjectId", + "type": "string", + "description": "SMUS project identifier" + }, + { + "name": "smusSpaceKey", + "type": "string", + "description": "SMUS space composite key consisting of domainId and spaceName" + }, + { + "name": "smusToolkitEnv", + "type": "string", + "description": "The environment user is running SMUS extension against" + }, + { + "name": "smusDomainRegion", + "type": "string", + "description": "The SMUS domain region" + }, + { + "name": "smusProjectRegion", + "type": "string", + "description": "The SMUS project region" + }, + { + "name": "smusConnectionId", + "type": "string", + "description": "SMUS connection identifier" + }, + { + "name": "smusConnectionType", + "type": "string", + "description": "SMUS connection type" + }, + { + "name": "smusDomainAccountId", + "type": "string", + "description": "SMUS domain account id" + }, + { + "name": "smusProjectAccountId", + "type": "string", + "description": "SMUS project account id" + }, + { + "name": "smusAuthMode", + "type": "string", + "allowedValues": ["sso", "iam"], + "description": "SMUS authentication mode (SSO or IAM)" } ], "metrics": [ @@ -277,6 +364,33 @@ } ] }, + { + "name": "hyperpod_openRemoteConnection", + "description": "Perform a connection to a Hyperpod Space", + "metadata": [ + { + "type": "result" + } + ] + }, + { + "name": "hyperpod_stopSpace", + "description": "Stop a HyperPod Space", + "metadata": [ + { + "type": "result" + } + ] + }, + { + "name": "hyperpod_filterSpaces", + "description": "Filter HyperPod Spaces", + "metadata": [ + { + "type": "result" + } + ] + }, { "name": "amazonq_didSelectProfile", "description": "Emitted after the user's Q Profile has been set, whether the user was prompted with a dialog, or a profile was automatically assigned after signing in.", @@ -1151,6 +1265,14 @@ "name": "appbuilder_lambda2sam", "description": "User click Convert a lambda function to SAM project" }, + { + "name": "auth_customEndpoint", + "description": "User used a custom endpoint" + }, + { + "name": "auth_localstackEndpoint", + "description": "User used a LocalStack connection" + }, { "name": "lambda_remoteDebugStop", "description": "user stop remote debugging", @@ -1257,6 +1379,375 @@ "required": false } ] + }, + { + "name": "amazonq_autoDebugCommand", + "description": "Tracks usage of Amazon Q auto debug commands (fixWithQ, fixAllWithQ, explainProblem)", + "metadata": [ + { + "type": "amazonqAutoDebugCommandType", + "required": true + }, + { + "type": "amazonqAutoDebugAction", + "required": true + }, + { + "type": "amazonqAutoDebugProblemCount", + "required": false + }, + { + "type": "result" + }, + { + "type": "reason", + "required": false + }, + { + "type": "reasonDesc", + "required": false + } + ] + }, + { + "name": "smus_login", + "description": "Emitted whenever a user signin to SMUS", + "metadata": [ + { + "type": "smusDomainId", + "required": false + }, + { + "type": "smusDomainAccountId", + "required": false + }, + { + "type": "smusAuthMode", + "required": false + } + ] + }, + { + "name": "smus_signOut", + "description": "Emitted whenever a user signouts SMUS", + "metadata": [ + { + "type": "smusDomainId", + "required": false + }, + { + "type": "smusDomainAccountId", + "required": false + }, + { + "type": "smusAuthMode", + "required": false + } + ] + }, + { + "name": "smus_accessProject", + "description": "Emitted whenever a user accesses a SMUS project", + "metadata": [ + { + "type": "smusDomainId", + "required": false + }, + { + "type": "smusDomainAccountId", + "required": false + }, + { + "type": "smusProjectId", + "required": false + }, + { + "type": "smusDomainRegion", + "required": false + }, + { + "type": "smusAuthMode", + "required": false + } + ] + }, + { + "name": "smus_renderProjectChildrenNode", + "description": "Emitted whenever children node of project is rendered", + "metadata": [ + { + "type": "smusToolkitEnv", + "required": false + }, + { + "type": "smusDomainId", + "required": false + }, + { + "type": "smusDomainAccountId", + "required": false + }, + { + "type": "smusProjectId", + "required": false + }, + { + "type": "smusDomainRegion", + "required": false + }, + { + "type": "smusAuthMode", + "required": false + } + ], + "passive": true + }, + { + "name": "smus_openRemoteConnection", + "description": "Emitted whenever a user starts a SMUS space", + "metadata": [ + { + "type": "smusDomainId", + "required": false + }, + { + "type": "smusDomainAccountId", + "required": false + }, + { + "type": "smusProjectId", + "required": false + }, + { + "type": "smusSpaceKey", + "required": false + }, + { + "type": "smusDomainRegion", + "required": false + }, + { + "type": "smusProjectRegion", + "required": false + }, + { + "type": "smusProjectAccountId", + "required": false + }, + { + "type": "smusAuthMode", + "required": false + } + ] + }, + { + "name": "smus_stopSpace", + "description": "Emitted whenever a user stop a SMUS space", + "metadata": [ + { + "type": "smusDomainId", + "required": false + }, + { + "type": "smusDomainAccountId", + "required": false + }, + { + "type": "smusProjectId", + "required": false + }, + { + "type": "smusSpaceKey", + "required": false + }, + { + "type": "smusDomainRegion", + "required": false + }, + { + "type": "smusProjectRegion", + "required": false + }, + { + "type": "smusProjectAccountId", + "required": false + }, + { + "type": "smusAuthMode", + "required": false + } + ] + }, + { + "name": "smus_renderS3Node", + "description": "Emitted whenever rendering a s3 node", + "metadata": [ + { + "type": "smusToolkitEnv", + "required": false + }, + { + "type": "smusDomainId", + "required": false + }, + { + "type": "smusDomainAccountId", + "required": false + }, + { + "type": "smusProjectId", + "required": false + }, + { + "type": "smusProjectRegion", + "required": false + }, + { + "type": "smusProjectAccountId", + "required": false + }, + { + "type": "smusConnectionId", + "required": false + }, + { + "type": "smusConnectionType", + "required": false + }, + { + "type": "smusAuthMode", + "required": false + } + ] + }, + { + "name": "smus_renderRedshiftNode", + "description": "Emitted whenever rendering a Redshift node", + "metadata": [ + { + "type": "smusToolkitEnv", + "required": false + }, + { + "type": "smusDomainId", + "required": false + }, + { + "type": "smusDomainAccountId", + "required": false + }, + { + "type": "smusProjectId", + "required": false + }, + { + "type": "smusProjectRegion", + "required": false + }, + { + "type": "smusProjectAccountId", + "required": false + }, + { + "type": "smusConnectionId", + "required": false + }, + { + "type": "smusConnectionType", + "required": false + }, + { + "type": "smusAuthMode", + "required": false + } + ] + }, + { + "name": "smus_renderLakehouseNode", + "description": "Emitted whenever rendering a Lakehouse node", + "metadata": [ + { + "type": "smusToolkitEnv", + "required": false + }, + { + "type": "smusDomainId", + "required": false + }, + { + "type": "smusDomainAccountId", + "required": false + }, + { + "type": "smusProjectId", + "required": false + }, + { + "type": "smusProjectRegion", + "required": false + }, + { + "type": "smusProjectAccountId", + "required": false + }, + { + "type": "smusConnectionId", + "required": false + }, + { + "type": "smusConnectionType", + "required": false + }, + { + "type": "smusAuthMode", + "required": false + } + ] + }, + { + "name": "smus_deeplinkConnect", + "description": "Emitted when a user connects to a SMUS space via deeplink", + "metadata": [ + { + "type": "result" + }, + { + "type": "reason", + "required": false + }, + { + "type": "smusDomainId", + "required": false + }, + { + "type": "smusDomainAccountId", + "required": false + }, + { + "type": "smusProjectId", + "required": false + }, + { + "type": "smusDomainRegion", + "required": false + }, + { + "type": "smusProjectRegion", + "required": false + }, + { + "type": "smusProjectAccountId", + "required": false + }, + { + "type": "smusSpaceKey", + "required": false + }, + { + "type": "smusAuthMode", + "required": false + } + ] } ] } diff --git a/packages/core/src/shared/ui/sam/stackPrompter.ts b/packages/core/src/shared/ui/sam/stackPrompter.ts index be1350489c5..3e86dc08859 100644 --- a/packages/core/src/shared/ui/sam/stackPrompter.ts +++ b/packages/core/src/shared/ui/sam/stackPrompter.ts @@ -2,7 +2,7 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -import { StackSummary } from 'aws-sdk/clients/cloudformation' +import { StackSummary } from '@aws-sdk/client-cloudformation' import { getAwsConsoleUrl } from '../../awsConsole' import { CloudFormationClient } from '../../clients/cloudFormation' import * as vscode from 'vscode' @@ -13,9 +13,10 @@ import { getRecentResponse } from '../../sam/utils' export const localize = nls.loadMessageBundle() -const canPickStack = (s: StackSummary) => s.StackStatus.endsWith('_COMPLETE') +const canPickStack = (s: StackSummary) => s.StackStatus?.endsWith('_COMPLETE') const canShowStack = (s: StackSummary) => - (s.StackStatus.endsWith('_COMPLETE') || s.StackStatus.endsWith('_IN_PROGRESS')) && !s.StackStatus.includes('DELETE') + (s.StackStatus?.endsWith('_COMPLETE') || s.StackStatus?.endsWith('_IN_PROGRESS')) && + !s.StackStatus.includes('DELETE') /** * Creates a quick pick prompter for choosing a CloudFormation stack diff --git a/packages/core/src/shared/utilities/cliUtils.ts b/packages/core/src/shared/utilities/cliUtils.ts index a37a7228687..bf19cd19791 100644 --- a/packages/core/src/shared/utilities/cliUtils.ts +++ b/packages/core/src/shared/utilities/cliUtils.ts @@ -55,7 +55,7 @@ interface Cli { exec?: string } -export type AwsClis = Extract +export type AwsClis = Extract /** * CLIs and their full filenames and download paths for their respective OSes @@ -170,6 +170,21 @@ export const awsClis: { [cli in AwsClis]: Cli } = { manualInstallLink: 'https://docs.docker.com/desktop', exec: 'docker', }, + // Currently Finch is available for MacOS and Linux; Windows support will be added if/when available + finch: { + command: { + unix: ['finch', path.join('/', 'usr', 'bin', 'finch'), path.join('/', 'usr', 'local', 'bin', 'finch')], + }, + source: { + macos: { + x86: 'https://github.com/runfinch/finch/releases/download/v1.11.0/Finch-v1.11.0-x86_64.pkg', + arm: 'https://github.com/runfinch/finch/releases/download/v1.11.0/Finch-v1.11.0-aarch64.pkg', + }, + }, + name: 'Finch', + manualInstallLink: 'https://runfinch.com/docs/getting-started/installation/', + exec: 'finch', + }, } /** @@ -185,7 +200,7 @@ export async function installCli( ): Promise { const cliToInstall = awsClis[cli] if (!cliToInstall) { - throw new InstallerError(`Invalid not found for CLI: ${cli}`) + throw new InstallerError(`Installer not found for CLI: ${cli}`) } let result: Result = 'Succeeded' let reason: string = '' @@ -247,10 +262,11 @@ export async function installCli( case 'aws-cli': case 'sam-cli': case 'docker': + case 'finch': cliPath = await installGui(cli, tempDir, progress, timeout) break default: - throw new InstallerError(`Invalid not found for CLI: ${cli}`) + throw new InstallerError(`Installer not found for CLI: ${cli}`) } } finally { timeout.dispose() diff --git a/packages/core/src/shared/utilities/functionUtils.ts b/packages/core/src/shared/utilities/functionUtils.ts index 214721b1cdb..fa0e61847bb 100644 --- a/packages/core/src/shared/utilities/functionUtils.ts +++ b/packages/core/src/shared/utilities/functionUtils.ts @@ -63,6 +63,32 @@ export function onceChanged(fn: (...args: U) => T): (...args : ((val = fn(...args)), (ran = true), (prevArgs = args.map(String).join(':')), val) } +/** + * Creates a function that runs only if the args changed versus the previous invocation, + * using a custom comparator function for argument comparison. + * + * @param fn The function to wrap + * @param comparator Function that returns true if arguments are equal + */ +export function onceChangedWithComparator( + fn: (...args: U) => T, + comparator: (prev: U, current: U) => boolean +): (...args: U) => T { + let val: T + let ran = false + let prevArgs: U + + return (...args) => { + if (ran && comparator(prevArgs, args)) { + return val + } + val = fn(...args) + ran = true + prevArgs = args + return val + } +} + /** * Creates a new function that stores the result of a call. * diff --git a/packages/core/src/shared/utilities/index.ts b/packages/core/src/shared/utilities/index.ts index a361834406c..e86f941456d 100644 --- a/packages/core/src/shared/utilities/index.ts +++ b/packages/core/src/shared/utilities/index.ts @@ -9,3 +9,4 @@ export * from './functionUtils' export * as messageUtils from './messages' export * as CommentUtils from './commentUtils' export * from './editorUtilities' +export * from './tsUtils' diff --git a/packages/core/src/shared/utilities/proxyUtil.ts b/packages/core/src/shared/utilities/proxyUtil.ts index 06150b9fc01..e617bcd85c3 100644 --- a/packages/core/src/shared/utilities/proxyUtil.ts +++ b/packages/core/src/shared/utilities/proxyUtil.ts @@ -11,6 +11,7 @@ interface ProxyConfig { noProxy: string | undefined proxyStrictSSL: boolean | true certificateAuthority: string | undefined + isProxyAndCertAutoDiscoveryEnabled: boolean } /** @@ -53,13 +54,15 @@ export class ProxyUtil { const amazonQConfig = vscode.workspace.getConfiguration('amazonQ') const proxySettings = amazonQConfig.get<{ certificateAuthority?: string - }>('proxy', {}) + enableProxyAndCertificateAutoDiscovery: boolean + }>('proxy', { enableProxyAndCertificateAutoDiscovery: true }) return { proxyUrl, noProxy, proxyStrictSSL, certificateAuthority: proxySettings.certificateAuthority, + isProxyAndCertAutoDiscoveryEnabled: proxySettings.enableProxyAndCertificateAutoDiscovery, } } @@ -67,8 +70,8 @@ export class ProxyUtil { * Sets environment variables based on proxy configuration */ private static async setProxyEnvironmentVariables(config: ProxyConfig): Promise { - // Always enable experimental proxy support for better handling of both explicit and transparent proxies - process.env.EXPERIMENTAL_HTTP_PROXY_SUPPORT = 'true' + // Set experimental proxy support based on user setting + process.env.EXPERIMENTAL_HTTP_PROXY_SUPPORT = config.isProxyAndCertAutoDiscoveryEnabled.toString() const proxyUrl = config.proxyUrl // Set proxy environment variables diff --git a/packages/core/src/shared/vscode/env.ts b/packages/core/src/shared/vscode/env.ts index abd9c58ae2d..1ddb042e415 100644 --- a/packages/core/src/shared/vscode/env.ts +++ b/packages/core/src/shared/vscode/env.ts @@ -6,12 +6,12 @@ import * as semver from 'semver' import * as vscode from 'vscode' import * as packageJson from '../../../package.json' -import * as os from 'os' import { getLogger } from '../logger/logger' import { onceChanged } from '../utilities/functionUtils' import { ChildProcess } from '../utilities/processUtils' import globals, { isWeb } from '../extensionGlobals' import * as devConfig from '../../dev/config' +import * as os from 'os' /** * Returns true if the current build is running on CI (build server). @@ -124,6 +124,35 @@ export function isRemoteWorkspace(): boolean { return vscode.env.remoteName === 'ssh-remote' } +/** + * Parses an os-release file according to the freedesktop.org standard. + * + * @param content The content of the os-release file + * @returns A record of key-value pairs from the os-release file + * + * @see https://www.freedesktop.org/software/systemd/man/latest/os-release.html + */ +function parseOsRelease(content: string): Record { + const result: Record = {} + + for (let line of content.split('\n')) { + line = line.trim() + // Skip empty lines and comments + if (!line || line.startsWith('#')) { + continue + } + + const eqIndex = line.indexOf('=') + if (eqIndex > 0) { + const key = line.slice(0, eqIndex) + const value = line.slice(eqIndex + 1).replace(/^["']|["']$/g, '') + result[key] = value + } + } + + return result +} + /** * Checks if the current environment has SageMaker-specific environment variables * @returns true if SageMaker environment variables are detected @@ -146,36 +175,83 @@ export function hasSageMakerEnvVars(): boolean { /** * Checks if the current environment is running on Amazon Linux 2. * - * This function attempts to detect if we're running in a container on an AL2 host - * by checking both the OS release and container-specific indicators. + * This function detects the container/runtime OS, not the host OS. + * In containerized environments, we check the container's OS identity. + * + * Detection Process (in order): + * 1. Returns false for web environments (browser-based) + * 2. Returns false for SageMaker environments (even if container is AL2) + * 3. Checks `/etc/os-release` with fallback to `/usr/lib/os-release` + * - Standard Linux OS identification files per freedesktop.org spec + * - Looks for `ID="amzn"` and `VERSION_ID="2"` for AL2 + * - This correctly identifies AL2 containers regardless of host OS + * + * This approach ensures correct detection in: + * - Containerized environments (detects container OS, not host) + * - AL2 containers on any host OS (Ubuntu, AL2023, etc.) + * - Web/browser environments (returns false) + * - SageMaker environments (returns false) * - * Example: `5.10.220-188.869.amzn2int.x86_64` or `5.10.236-227.928.amzn2.x86_64` (Cloud Dev Machine) + * Note: We intentionally do NOT check kernel version as it reflects the host OS, + * not the container OS. AL2 containers should be treated as AL2 environments + * regardless of whether they run on AL2, Ubuntu, or other host kernels. + * + * References: + * - https://docs.aws.amazon.com/linux/al2/ug/ident-amazon-linux-specific.html + * - https://docs.aws.amazon.com/linux/al2/ug/ident-os-release.html + * - https://www.freedesktop.org/software/systemd/man/latest/os-release.html */ export function isAmazonLinux2() { + // Skip AL2 detection for web environments + // In web mode, we're running in a browser, not on AL2 + if (isWeb()) { + return false + } + // First check if we're in a SageMaker environment, which should not be treated as AL2 - // even if the underlying host is AL2 + // even if the underlying container is AL2 if (hasSageMakerEnvVars()) { return false } - // Check if we're in a container environment that's not AL2 - if (process.env.container === 'docker' || process.env.DOCKER_HOST || process.env.DOCKER_BUILDKIT) { - // Additional check for container OS - if we can determine it's not AL2 - try { - const fs = require('fs') - if (fs.existsSync('/etc/os-release')) { - const osRelease = fs.readFileSync('/etc/os-release', 'utf8') - if (!osRelease.includes('Amazon Linux 2') && !osRelease.includes('amzn2')) { - return false + // Only proceed with file checks on Linux platforms + if (process.platform !== 'linux') { + return false + } + + // Check the container/runtime OS identity via os-release files + // This correctly identifies AL2 containers regardless of host OS + try { + const fs = require('fs') + // Check /etc/os-release with fallback to /usr/lib/os-release as per freedesktop.org spec + const osReleasePaths = ['/etc/os-release', '/usr/lib/os-release'] + + for (const osReleasePath of osReleasePaths) { + if (fs.existsSync(osReleasePath)) { + try { + const osReleaseContent = fs.readFileSync(osReleasePath, 'utf8') + const osRelease = parseOsRelease(osReleaseContent) + + // Check if this is Amazon Linux 2 + // We trust os-release as the authoritative source for container OS identity + return osRelease.VERSION_ID === '2' && osRelease.ID === 'amzn' + } catch (e) { + // Continue to next path if parsing fails + getLogger().error(`Parsing os-release file ${osReleasePath} failed: ${e}`) } } - } catch (e) { - // If we can't read the file, fall back to the os.release() check } + } catch (e) { + // If we can't read the files, we cannot determine AL2 status + getLogger().error(`Checking os-release files failed: ${e}`) } - // Standard check for AL2 in the OS release string - return (os.release().includes('.amzn2int.') || os.release().includes('.amzn2.')) && process.platform === 'linux' + // Fall back to kernel version check if os-release files are unavailable or failed + // This is needed for environments where os-release might not be accessible + const kernelRelease = os.release() + const hasAL2Kernel = kernelRelease.includes('.amzn2int.') || kernelRelease.includes('.amzn2.') + + return hasAL2Kernel } /** @@ -217,9 +293,9 @@ export function getExtRuntimeContext(): { extensionHost: ExtensionHostLocation } { const extensionHost = - // taken from https://github.com/microsoft/vscode/blob/7c9e4bb23992c63f20cd86bbe7a52a3aa4bed89d/extensions/github-authentication/src/githubServer.ts#L121 to help determine which auth flows - // should be used - typeof navigator === 'undefined' + // Check if we're in a Node.js environment (desktop/remote) vs web worker + // Updated to be compatible with Node.js v22 which includes navigator global + typeof process === 'object' && process.versions?.node ? globals.context.extension.extensionKind === vscode.ExtensionKind.UI ? 'local' : 'remote' diff --git a/packages/core/src/shared/vscode/setContext.ts b/packages/core/src/shared/vscode/setContext.ts index 7cfaf4092f8..ca594f9e9f6 100644 --- a/packages/core/src/shared/vscode/setContext.ts +++ b/packages/core/src/shared/vscode/setContext.ts @@ -28,8 +28,24 @@ export type contextKey = | 'aws.toolkit.amazonq.dismissed' | 'aws.toolkit.amazonqInstall.dismissed' | 'aws.stepFunctions.isWorkflowStudioFocused' + | 'aws.cloudformation.stacks.diffVisible' + | 'aws.cloudformation.stackSelected' + | 'aws.cloudformation.changeSetMode' + | 'aws.cloudformation.loadingStacks' + | 'aws.cloudformation.loadingResources' + | 'aws.cloudformation.importingResource' + | 'aws.cloudformation.cloningResource' + | 'aws.cloudformation.gettingStackMgmtInfo' + | 'aws.cloudformation.refreshingResourceList' + | 'aws.cloudformation.refreshingAllResources' + | 'aws.cloudformation.refreshingStacks' + | 'aws.cloudformation.stacks.detailVisible' | 'aws.toolkit.notifications.show' | 'aws.amazonq.editSuggestionActive' + | 'aws.smus.connected' + | 'aws.smus.inSmusSpaceEnvironment' + | 'aws.cloudFormation.serviceEnabled' + | 'aws.smus.isIamMode' // Deprecated/legacy names. New keys should start with "aws.". | 'codewhisperer.activeLine' | 'gumby.isPlanAvailable' diff --git a/packages/core/src/shared/vscode/uriHandler.ts b/packages/core/src/shared/vscode/uriHandler.ts index c8beda72fc4..3f7411b2bbe 100644 --- a/packages/core/src/shared/vscode/uriHandler.ts +++ b/packages/core/src/shared/vscode/uriHandler.ts @@ -47,7 +47,10 @@ export class UriHandler implements vscode.UriHandler { let parsedQuery: Parameters[0] // Ensure '+' is treated as a literal plus sign, not a space, by encoding it as '%2B' - const url = new URL(uri.toString(true).replace(/\+/g, '%2B')) + // Also decode HTML entities like & to & to ensure proper parameter parsing + const originalUri = uri.toString(true) + const uriString = originalUri.replace(/&/g, '&').replace(/\+/g, '%2B') + const url = new URL(uriString) const params = new SearchParams(url.searchParams) try { diff --git a/packages/core/src/ssmDocument/commands/openDocumentItem.ts b/packages/core/src/ssmDocument/commands/openDocumentItem.ts index af0c6760358..07832d0b560 100644 --- a/packages/core/src/ssmDocument/commands/openDocumentItem.ts +++ b/packages/core/src/ssmDocument/commands/openDocumentItem.ts @@ -6,7 +6,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { SSM } from 'aws-sdk' +import { DocumentFormat, DocumentVersionInfo } from '@aws-sdk/client-ssm' import * as vscode from 'vscode' import { DocumentItemNode } from '../explorer/documentItemNode' import { AwsContext } from '../../shared/awsContext' @@ -16,7 +16,7 @@ import { showViewLogsMessage } from '../../shared/utilities/messages' import { telemetry } from '../../shared/telemetry/telemetry' import { Result } from '../../shared/telemetry/telemetry' -export async function openDocumentItem(node: DocumentItemNode, awsContext: AwsContext, format?: string) { +export async function openDocumentItem(node: DocumentItemNode, awsContext: AwsContext, format?: DocumentFormat) { const logger: Logger = getLogger() let result: Result = 'Succeeded' @@ -65,7 +65,7 @@ export async function openDocumentItemYaml(node: DocumentItemNode, awsContext: A await openDocumentItem(node, awsContext, 'YAML') } -async function promptUserforDocumentVersion(versions: SSM.Types.DocumentVersionInfo[]): Promise { +async function promptUserforDocumentVersion(versions: DocumentVersionInfo[]): Promise { // Prompt user to pick document version const quickPickItems: vscode.QuickPickItem[] = [] for (const version of versions) { diff --git a/packages/core/src/ssmDocument/commands/publishDocument.ts b/packages/core/src/ssmDocument/commands/publishDocument.ts index 402c86412a6..36a3145b952 100644 --- a/packages/core/src/ssmDocument/commands/publishDocument.ts +++ b/packages/core/src/ssmDocument/commands/publishDocument.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SSM } from 'aws-sdk' +import { CreateDocumentRequest, UpdateDocumentRequest } from '@aws-sdk/client-ssm' import * as vscode from 'vscode' import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() @@ -75,7 +75,7 @@ export async function createDocument( logger.info(`Creating Systems Manager Document '${wizardResponse.name}'`) try { - const request: SSM.CreateDocumentRequest = { + const request: CreateDocumentRequest = { Content: textDocument.getText(), Name: wizardResponse.name, DocumentType: wizardResponse.documentType, @@ -109,7 +109,7 @@ export async function updateDocument( logger.info(`Updating Systems Manager Document '${wizardResponse.name}'`) try { - const request: SSM.UpdateDocumentRequest = { + const request: UpdateDocumentRequest = { Content: textDocument.getText(), Name: wizardResponse.name, DocumentVersion: '$LATEST', diff --git a/packages/core/src/ssmDocument/commands/updateDocumentVersion.ts b/packages/core/src/ssmDocument/commands/updateDocumentVersion.ts index cb3c1577360..2574ed2f70d 100644 --- a/packages/core/src/ssmDocument/commands/updateDocumentVersion.ts +++ b/packages/core/src/ssmDocument/commands/updateDocumentVersion.ts @@ -6,7 +6,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { SSM } from 'aws-sdk' +import { DocumentVersionInfo } from '@aws-sdk/client-ssm' import * as vscode from 'vscode' import { AwsContext } from '../../shared/awsContext' import { getLogger, Logger } from '../../shared/logger/logger' @@ -76,7 +76,7 @@ export async function updateDocumentVersion(node: DocumentItemNodeWriteable, aws } } -async function promptUserforDocumentVersion(versions: SSM.Types.DocumentVersionInfo[]): Promise { +async function promptUserforDocumentVersion(versions: DocumentVersionInfo[]): Promise { // Prompt user to pick document version const quickPickItems: vscode.QuickPickItem[] = [] for (const version of versions) { diff --git a/packages/core/src/ssmDocument/explorer/documentItemNode.ts b/packages/core/src/ssmDocument/explorer/documentItemNode.ts index 1fb06df96d1..ee17e53d41f 100644 --- a/packages/core/src/ssmDocument/explorer/documentItemNode.ts +++ b/packages/core/src/ssmDocument/explorer/documentItemNode.ts @@ -3,8 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SSM } from 'aws-sdk' - +import { DocumentFormat, DocumentIdentifier, DocumentVersionInfo, GetDocumentResult } from '@aws-sdk/client-ssm' import { SsmDocumentClient } from '../../shared/clients/ssmDocumentClient' import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' @@ -13,7 +12,7 @@ import { getIcon } from '../../shared/icons' export class DocumentItemNode extends AWSTreeNodeBase { public constructor( - private documentItem: SSM.Types.DocumentIdentifier, + private documentItem: DocumentIdentifier, public readonly client: SsmDocumentClient, public override readonly regionCode: string ) { @@ -23,7 +22,7 @@ export class DocumentItemNode extends AWSTreeNodeBase { this.iconPath = getIcon('vscode-file') } - public update(documentItem: SSM.Types.DocumentIdentifier): void { + public update(documentItem: DocumentIdentifier): void { this.documentItem = documentItem this.label = this.documentName } @@ -38,13 +37,13 @@ export class DocumentItemNode extends AWSTreeNodeBase { public async getDocumentContent( documentVersion?: string, - documentFormat?: string - ): Promise { + documentFormat?: DocumentFormat + ): Promise { if (!this.documentName || !this.documentName.length) { return Promise.resolve({}) } - let resolvedDocumentFormat: string | undefined + let resolvedDocumentFormat: DocumentFormat | undefined if (documentFormat === undefined) { // retrieves the document format from the service @@ -61,7 +60,7 @@ export class DocumentItemNode extends AWSTreeNodeBase { ) } - public async listSchemaVersion(): Promise { + public async listSchemaVersion(): Promise { return await toArrayAsync(this.client.listDocumentVersions(this.documentName)) } } diff --git a/packages/core/src/ssmDocument/explorer/documentItemNodeWriteable.ts b/packages/core/src/ssmDocument/explorer/documentItemNodeWriteable.ts index 75a9a011d2b..4c2ffd6e813 100644 --- a/packages/core/src/ssmDocument/explorer/documentItemNodeWriteable.ts +++ b/packages/core/src/ssmDocument/explorer/documentItemNodeWriteable.ts @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SSM } from 'aws-sdk' +import { DeleteDocumentResult, DocumentIdentifier, UpdateDocumentDefaultVersionResult } from '@aws-sdk/client-ssm' import { RegistryItemNode } from './registryItemNode' import { SsmDocumentClient } from '../../shared/clients/ssmDocumentClient' import { DocumentItemNode } from './documentItemNode' export class DocumentItemNodeWriteable extends DocumentItemNode { public constructor( - documentItem: SSM.Types.DocumentIdentifier, + documentItem: DocumentIdentifier, public override readonly client: SsmDocumentClient, public override readonly regionCode: string, public readonly parent: RegistryItemNode @@ -20,7 +20,7 @@ export class DocumentItemNodeWriteable extends DocumentItemNode { this.parent = parent } - public async deleteDocument(): Promise { + public async deleteDocument(): Promise { if (!this.documentName || !this.documentName.length) { return Promise.resolve({}) } @@ -28,9 +28,7 @@ export class DocumentItemNodeWriteable extends DocumentItemNode { return await this.client.deleteDocument(this.documentName) } - public async updateDocumentVersion( - documentVersion?: string - ): Promise { + public async updateDocumentVersion(documentVersion?: string): Promise { if (!documentVersion || !documentVersion.length) { return Promise.resolve({}) } diff --git a/packages/core/src/ssmDocument/explorer/registryItemNode.ts b/packages/core/src/ssmDocument/explorer/registryItemNode.ts index d5ca928f88f..dae0771f67d 100644 --- a/packages/core/src/ssmDocument/explorer/registryItemNode.ts +++ b/packages/core/src/ssmDocument/explorer/registryItemNode.ts @@ -6,7 +6,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { SSM } from 'aws-sdk' +import { DocumentIdentifier, ListDocumentsRequest } from '@aws-sdk/client-ssm' import * as vscode from 'vscode' import { DefaultSsmDocumentClient, SsmDocumentClient } from '../../shared/clients/ssmDocumentClient' @@ -64,8 +64,8 @@ export class RegistryItemNode extends AWSTreeNodeBase { }) } - private async getDocumentByOwner(client: SsmDocumentClient): Promise { - const request: SSM.ListDocumentsRequest = { + private async getDocumentByOwner(client: SsmDocumentClient): Promise { + const request: ListDocumentsRequest = { Filters: [ { Key: 'DocumentType', @@ -95,7 +95,7 @@ export class RegistryItemNode extends AWSTreeNodeBase { } public async updateChildren(): Promise { - const documents = new Map() + const documents = new Map() const docs = await this.getDocumentByOwner(this.client) for (const doc of docs) { diff --git a/packages/core/src/ssmDocument/ssm/ssmClient.ts b/packages/core/src/ssmDocument/ssm/ssmClient.ts index 2e9c0c19b30..1fce41b2cea 100644 --- a/packages/core/src/ssmDocument/ssm/ssmClient.ts +++ b/packages/core/src/ssmDocument/ssm/ssmClient.ts @@ -13,16 +13,10 @@ const localize = nls.loadMessageBundle() import { ExtensionContext, LanguageConfiguration, languages, window, workspace } from 'vscode' -import { - DidChangeConfigurationNotification, - LanguageClient, - LanguageClientOptions, - NotificationType, - ServerOptions, - TransportKind, -} from 'vscode-languageclient' +import { DidChangeConfigurationNotification, LanguageClientOptions, NotificationType } from 'vscode-languageclient' +import { ServerOptions, TransportKind, LanguageClient } from 'vscode-languageclient/node' -export const ResultLimitReached: NotificationType = new NotificationType('ssm/resultLimitReached') +export const ResultLimitReached: NotificationType = new NotificationType('ssm/resultLimitReached') const jsonLanguageConfiguration: LanguageConfiguration = { wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/, @@ -108,14 +102,14 @@ export async function activate(extensionContext: ExtensionContext) { ) client.registerProposedFeatures() - const disposable = client.start() - toDispose.push(disposable) + void client.start() + toDispose.push(client) languages.setLanguageConfiguration('ssm-json', jsonLanguageConfiguration) languages.setLanguageConfiguration('ssm-yaml', yamlLanguageConfiguration) - return client.onReady().then(() => { - client.onNotification(ResultLimitReached, (message) => { + return client.start().then(() => { + client.onNotification(ResultLimitReached, (message: any) => { void window.showInformationMessage( `${message}\nUse setting 'aws.ssmDocument.ssm.maxItemsComputed' to configure the limit.` ) diff --git a/packages/core/src/ssmDocument/wizards/publishDocumentWizard.ts b/packages/core/src/ssmDocument/wizards/publishDocumentWizard.ts index 74a786b74c7..763c9995933 100644 --- a/packages/core/src/ssmDocument/wizards/publishDocumentWizard.ts +++ b/packages/core/src/ssmDocument/wizards/publishDocumentWizard.ts @@ -6,7 +6,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { SSM } from 'aws-sdk' +import { DocumentKeyValuesFilter } from '@aws-sdk/client-ssm' import { createCommonButtons } from '../../shared/ui/buttons' import { createRegionPrompter } from '../../shared/ui/common/region' import { createInputBox } from '../../shared/ui/inputPrompter' @@ -27,8 +27,8 @@ export enum PublishSSMDocumentAction { QuickUpdate = 'Update', } -async function* loadDocuments(region: string, documentType?: SSM.Types.DocumentType) { - const filters: SSM.Types.DocumentKeyValuesFilterList = [ +async function* loadDocuments(region: string, documentType?: string) { + const filters: DocumentKeyValuesFilter[] = [ { Key: 'Owner', Values: ['Self'], diff --git a/packages/core/src/stepFunctions/asl/aslServer.ts b/packages/core/src/stepFunctions/asl/aslServer.ts index 2d4c4fadde3..7a55cfa5ed5 100644 --- a/packages/core/src/stepFunctions/asl/aslServer.ts +++ b/packages/core/src/stepFunctions/asl/aslServer.ts @@ -25,7 +25,7 @@ import { Diagnostic, Disposable, DocumentRangeFormattingRequest, - IConnection, + Connection, InitializeParams, InitializeResult, NotificationType, @@ -33,6 +33,7 @@ import { ServerCapabilities, TextDocuments, TextDocumentSyncKind, + DidChangeWatchedFilesParams, } from 'vscode-languageserver' import { posix } from 'path' @@ -41,12 +42,12 @@ import { getLanguageModelCache } from '../../shared/lsp/languageModelCache' import { formatError, runSafe, runSafeAsync } from '../../shared/lsp/utils/runner' import { YAML_ASL, JSON_ASL } from '../constants/aslFormats' -export const ResultLimitReached: NotificationType = new NotificationType('asl/resultLimitReached') +export const ResultLimitReached: NotificationType = new NotificationType('asl/resultLimitReached') -export const ForceValidateRequest: RequestType = new RequestType('asl/validate') +export const ForceValidateRequest: RequestType = new RequestType('asl/validate') // Create a connection for the server -const connection: IConnection = createConnection() +const connection: Connection = (createConnection as any)() process.on('unhandledRejection', (e: any) => { console.error(formatError('Unhandled exception', e)) @@ -179,7 +180,7 @@ class LimitExceededWarnings { } else { warning = { features: { [name]: name } } warning.timeout = setTimeout(() => { - connection.sendNotification( + void connection.sendNotification( ResultLimitReached, `${posix.basename(uri)}: For performance reasons, ${Object.keys(warning.features).join( ' and ' @@ -195,7 +196,7 @@ class LimitExceededWarnings { let formatterRegistration: Thenable | undefined -connection.onDidChangeConfiguration((change) => { +connection.onDidChangeConfiguration((change: any) => { const settings = change.settings foldingRangeLimit = Math.trunc( @@ -225,7 +226,7 @@ connection.onDidChangeConfiguration((change) => { }) // Retry schema validation on all open documents -connection.onRequest(ForceValidateRequest, async (uri) => { +connection.onRequest(ForceValidateRequest, async (uri: any) => { return new Promise((resolve) => { const document = documents.get(uri) if (document) { @@ -241,7 +242,7 @@ connection.onRequest(ForceValidateRequest, async (uri) => { // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. -documents.onDidChangeContent((change) => { +documents.onDidChangeContent((change: any) => { LimitExceededWarnings.cancel(change.document.uri) triggerValidation(change.document) }) @@ -250,7 +251,7 @@ documents.onDidChangeContent((change) => { documents.onDidClose((event) => { LimitExceededWarnings.cancel(event.document.uri) cleanPendingValidation(event.document) - connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }) + void connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }) }) const pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {} @@ -283,7 +284,7 @@ function getLanguageService(langId: string): LanguageService { function validateTextDocument(textDocument: TextDocument, callback?: (diagnostics: Diagnostic[]) => void): void { const respond = (diagnostics: Diagnostic[]) => { - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }) + void connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }) if (callback) { callback(diagnostics) } @@ -314,7 +315,7 @@ function validateTextDocument(textDocument: TextDocument, callback?: (diagnostic ) } -connection.onDidChangeWatchedFiles((change) => { +connection.onDidChangeWatchedFiles((change: DidChangeWatchedFilesParams) => { // Monitored files have changed in VSCode let hasChanges = false for (const c of change.changes) { diff --git a/packages/core/src/stepFunctions/asl/client.ts b/packages/core/src/stepFunctions/asl/client.ts index ce16850d5d6..fff743252ad 100644 --- a/packages/core/src/stepFunctions/asl/client.ts +++ b/packages/core/src/stepFunctions/asl/client.ts @@ -32,17 +32,15 @@ import { DidChangeConfigurationNotification, DocumentRangeFormattingParams, DocumentRangeFormattingRequest, - LanguageClient, LanguageClientOptions, NotificationType, - ServerOptions, - TransportKind, } from 'vscode-languageclient' +import { ServerOptions, TransportKind, LanguageClient } from 'vscode-languageclient/node' import { YAML_ASL, JSON_ASL, ASL_FORMATS } from '../constants/aslFormats' import { StepFunctionsSettings } from '../utils' -export const ResultLimitReached: NotificationType = new NotificationType('asl/resultLimitReached') +export const ResultLimitReached: NotificationType = new NotificationType('asl/resultLimitReached') interface Settings { asl?: { @@ -116,8 +114,8 @@ export class ASLLanguageClient { ) client.registerProposedFeatures() - const disposable = client.start() - toDispose.push(disposable) + void client.start() + toDispose.push(client) const languageConfiguration: LanguageConfiguration = { wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/, @@ -144,29 +142,35 @@ export class ASLLanguageClient { const params: DocumentRangeFormattingParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), range: client.code2ProtocolConverter.asRange(range), - options: client.code2ProtocolConverter.asFormattingOptions(options), + options: (client.code2ProtocolConverter as any).asFormattingOptions(options, {}), } - return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( - (response) => client.protocol2CodeConverter.asTextEdits(response), - async (error) => { - client.logFailedRequest(DocumentRangeFormattingRequest.type, error) - - return Promise.resolve([]) - } - ) + return (client as any) + .sendRequest(DocumentRangeFormattingRequest.method, params, undefined) + .then( + (response: any) => client.protocol2CodeConverter.asTextEdits(response), + async () => { + return Promise.resolve([]) + } + ) }, }) } } - return client.onReady().then(() => { + return client.start().then(() => { updateFormatterRegistration() const disposableFunc = { dispose: () => rangeFormatting?.dispose() as void } toDispose.push(disposableFunc) - toDispose.push(config.onDidChange(({ key }) => key === 'format.enable' && updateFormatterRegistration())) + toDispose.push( + config.onDidChange(({ key }) => { + if (key === 'format.enable') { + updateFormatterRegistration() + } + }) + ) - client.onNotification(ResultLimitReached, (message) => { + client.onNotification(ResultLimitReached, (message: any) => { void window.showInformationMessage( `${message}\nUse setting 'aws.stepfunctions.asl.maxItemsComputed' to configure the limit.` ) diff --git a/packages/core/src/stepFunctions/workflowStudio/types.ts b/packages/core/src/stepFunctions/workflowStudio/types.ts index 5edf944eb2c..22a93a8404d 100644 --- a/packages/core/src/stepFunctions/workflowStudio/types.ts +++ b/packages/core/src/stepFunctions/workflowStudio/types.ts @@ -2,8 +2,8 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -import { IAM } from 'aws-sdk' -import * as StepFunctions from '@aws-sdk/client-sfn' +import { ListRolesCommandInput } from '@aws-sdk/client-iam' +import { TestStateInput } from '@aws-sdk/client-sfn' import * as vscode from 'vscode' export enum WorkflowMode { @@ -94,8 +94,8 @@ export enum ApiAction { } type ApiCallRequestMapping = { - [ApiAction.IAMListRoles]: IAM.ListRolesRequest - [ApiAction.SFNTestState]: StepFunctions.TestStateInput + [ApiAction.IAMListRoles]: ListRolesCommandInput + [ApiAction.SFNTestState]: TestStateInput } interface ApiCallRequestMessageBase extends Message { diff --git a/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts b/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts index b286ad6537f..f5a92299255 100644 --- a/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts +++ b/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts @@ -98,23 +98,51 @@ describe('Transformation History Handler', function () { it('Creates history file with headers when it does not exist', async function () { sinon.stub(fs, 'existsFile').resolves(false) - await writeToHistoryFile('01/01/25, 10:00 AM', 'test-project', 'COMPLETED', '5 min', 'job-123', '/job/path') + await writeToHistoryFile( + '01/01/25, 10:00 AM', + 'test-project', + 'COMPLETED', + '5 min', + 'job-123', + '/job/path', + 'LANGUAGE_UPGRADE', + 'JDK8', + 'JDK17', + '/path/here', + 'clean test-compile' + ) const expectedPath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') const fileContent = writtenFiles.get(expectedPath) assert(fileContent) - assert(fileContent.includes('date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n')) assert( fileContent.includes( - `01/01/25, 10:00 AM\ttest-project\tCOMPLETED\t5 min\t${path.join('/job/path', 'diff.patch')}\t${path.join('/job/path', 'summary', 'summary.md')}\tjob-123\n` + 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\ttransformation_type\tsource_jdk_version\ttarget_jdk_version\tcustom_dependency_version_file_path\tcustom_build_command\n' + ) + ) + assert( + fileContent.includes( + `01/01/25, 10:00 AM\ttest-project\tCOMPLETED\t5 min\t${path.join('/job/path', 'diff.patch')}\t${path.join('/job/path', 'summary', 'summary.md')}\tjob-123\tLANGUAGE_UPGRADE\tJDK8\tJDK17\t/path/here\tclean test-compile\n` ) ) }) it('Excludes artifact paths for failed jobs', async function () { sinon.stub(fs, 'existsFile').resolves(false) - await writeToHistoryFile('01/01/25, 10:00 AM', 'test-project', 'FAILED', '5 min', 'job-123', '/job/path') + await writeToHistoryFile( + '01/01/25, 10:00 AM', + 'test-project', + 'FAILED', + '5 min', + 'job-123', + '/job/path', + 'LANGUAGE_UPGRADE', + 'JDK8', + 'JDK17', + '/path/here', + 'clean test-compile' + ) const expectedPath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') const fileContent = writtenFiles.get(expectedPath) @@ -130,7 +158,7 @@ describe('Transformation History Handler', function () { it('Appends new job to existing history file', async function () { const existingContent = 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n' + - '12/31/24, 09:00 AM\told-project\tCOMPLETED\t3 min\t/old/diff.patch\t/old/summary.md\told-job-456\n' + '12/31/24, 09:00 AM\told-project\tCOMPLETED\t3 min\t/old/diff.patch\t/old/summary.md\told-job-456\t/old/path\tLANGUAGE_UPGRADE\tJDK8\tJDK17\t/old/path2\tclean test-compile\n' writtenFiles.set( path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv'), @@ -139,14 +167,28 @@ describe('Transformation History Handler', function () { sinon.stub(fs, 'existsFile').resolves(true) - await writeToHistoryFile('01/01/25, 10:00 AM', 'new-project', 'FAILED', '2 min', 'new-job-789', '/new/path') + await writeToHistoryFile( + '01/01/25, 10:00 AM', + 'new-project', + 'FAILED', + '2 min', + 'new-job-789', + '/new/path', + 'LANGUAGE_UPGRADE', + 'JDK8', + 'JDK17', + '/path/here', + 'clean test-compile' + ) const expectedPath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') const fileContent = writtenFiles.get(expectedPath) // Verify old data is preserved assert( - fileContent?.includes('old-project\tCOMPLETED\t3 min\t/old/diff.patch\t/old/summary.md\told-job-456') + fileContent?.includes( + 'old-project\tCOMPLETED\t3 min\t/old/diff.patch\t/old/summary.md\told-job-456\t/old/path\tLANGUAGE_UPGRADE\tJDK8\tJDK17\t/old/path2\tclean test-compile\n' + ) ) // Verify new data is added diff --git a/packages/core/src/test/auth/credentials/utils.test.ts b/packages/core/src/test/auth/credentials/utils.test.ts new file mode 100644 index 00000000000..dac7095dd37 --- /dev/null +++ b/packages/core/src/test/auth/credentials/utils.test.ts @@ -0,0 +1,65 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { Credentials } from '@aws-sdk/types' +import { asEnvironmentVariables } from '../../../auth/credentials/utils' + +describe('asEnvironmentVariables', function () { + const testCredentials: Credentials = { + accessKeyId: 'test-access-key', + secretAccessKey: 'test-secret-key', + sessionToken: 'test-session-token', + } + + it('converts credentials to environment variables', function () { + const envVars = asEnvironmentVariables(testCredentials) + + assert.strictEqual(envVars.AWS_ACCESS_KEY, testCredentials.accessKeyId) + assert.strictEqual(envVars.AWS_ACCESS_KEY_ID, testCredentials.accessKeyId) + assert.strictEqual(envVars.AWS_SECRET_ACCESS_KEY, testCredentials.secretAccessKey) + assert.strictEqual(envVars.AWS_SESSION_TOKEN, testCredentials.sessionToken) + assert.strictEqual(envVars.AWS_SECURITY_TOKEN, testCredentials.sessionToken) + }) + + it('includes endpoint URL when provided', function () { + const testEndpointUrl = 'https://custom-endpoint.example.com' + const envVars = asEnvironmentVariables(testCredentials, testEndpointUrl) + + assert.strictEqual(envVars.AWS_ACCESS_KEY, testCredentials.accessKeyId) + assert.strictEqual(envVars.AWS_ACCESS_KEY_ID, testCredentials.accessKeyId) + assert.strictEqual(envVars.AWS_SECRET_ACCESS_KEY, testCredentials.secretAccessKey) + assert.strictEqual(envVars.AWS_SESSION_TOKEN, testCredentials.sessionToken) + assert.strictEqual(envVars.AWS_SECURITY_TOKEN, testCredentials.sessionToken) + assert.strictEqual(envVars.AWS_ENDPOINT_URL, testEndpointUrl) + }) + + it('does not include endpoint URL when not provided', function () { + const envVars = asEnvironmentVariables(testCredentials) + + assert.strictEqual(envVars.AWS_ACCESS_KEY, testCredentials.accessKeyId) + assert.strictEqual(envVars.AWS_ACCESS_KEY_ID, testCredentials.accessKeyId) + assert.strictEqual(envVars.AWS_SECRET_ACCESS_KEY, testCredentials.secretAccessKey) + assert.strictEqual(envVars.AWS_SESSION_TOKEN, testCredentials.sessionToken) + assert.strictEqual(envVars.AWS_SECURITY_TOKEN, testCredentials.sessionToken) + assert.strictEqual(envVars.AWS_ENDPOINT_URL, undefined) + }) + + it('handles credentials without session token', function () { + const credsWithoutToken: Credentials = { + accessKeyId: 'test-access-key', + secretAccessKey: 'test-secret-key', + } + const testEndpointUrl = 'https://custom-endpoint.example.com' + const envVars = asEnvironmentVariables(credsWithoutToken, testEndpointUrl) + + assert.strictEqual(envVars.AWS_ACCESS_KEY, credsWithoutToken.accessKeyId) + assert.strictEqual(envVars.AWS_ACCESS_KEY_ID, credsWithoutToken.accessKeyId) + assert.strictEqual(envVars.AWS_SECRET_ACCESS_KEY, credsWithoutToken.secretAccessKey) + assert.strictEqual(envVars.AWS_SESSION_TOKEN, undefined) + assert.strictEqual(envVars.AWS_SECURITY_TOKEN, undefined) + assert.strictEqual(envVars.AWS_ENDPOINT_URL, testEndpointUrl) + }) +}) diff --git a/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts b/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts index 1884e16e984..cb8ce40821b 100644 --- a/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts +++ b/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts @@ -77,3 +77,110 @@ describe('SharedCredentialsProvider - Role Chaining with SSO', function () { assert.strictEqual(credentials.sessionToken, 'assumed-session-token') }) }) + +describe('SharedCredentialsProvider - Endpoint URL', function () { + it('returns endpoint URL when present in profile', async function () { + const ini = ` + [profile test-profile] + aws_access_key_id = test-key + aws_secret_access_key = test-secret + endpoint_url = https://custom-endpoint.example.com + region = us-west-2 + ` + const sections = await createTestSections(ini) + const provider = new SharedCredentialsProvider('test-profile', sections) + + assert.strictEqual(provider.getEndpointUrl(), 'https://custom-endpoint.example.com') + }) + + it('returns undefined when endpoint URL is not present in profile', async function () { + const ini = ` + [profile test-profile] + aws_access_key_id = test-key + aws_secret_access_key = test-secret + region = us-west-2 + ` + const sections = await createTestSections(ini) + const provider = new SharedCredentialsProvider('test-profile', sections) + + assert.strictEqual(provider.getEndpointUrl(), undefined) + }) + + it('returns endpoint URL for SSO profile', async function () { + const ini = ` + [sso-session sso-valerena] + sso_start_url = https://example.awsapps.com/start + sso_region = us-east-1 + sso_registration_scopes = sso:account:access + [profile sso-profile] + sso_account_id = 123456789012 + sso_role_name = TestRole + region = us-west-2 + endpoint_url = https://sso-endpoint.example.com + ` + const sections = await createTestSections(ini) + const provider = new SharedCredentialsProvider('sso-profile', sections) + + assert.strictEqual(provider.getEndpointUrl(), 'https://sso-endpoint.example.com') + }) + + it('returns endpoint URL for role assumption profile', async function () { + const ini = ` + [profile source-profile] + aws_access_key_id = source-key + aws_secret_access_key = source-secret + + [profile role-profile] + role_arn = arn:aws:iam::123456789012:role/TestRole + source_profile = source-profile + region = us-west-2 + endpoint_url = https://role-endpoint.example.com + ` + const sections = await createTestSections(ini) + const provider = new SharedCredentialsProvider('role-profile', sections) + + assert.strictEqual(provider.getEndpointUrl(), 'https://role-endpoint.example.com') + }) + + it('returns endpoint URL for credential process profile', async function () { + const ini = ` + [profile process-profile] + credential_process = /usr/local/bin/credential-process + region = us-west-2 + endpoint_url = https://process-endpoint.example.com + ` + const sections = await createTestSections(ini) + const provider = new SharedCredentialsProvider('process-profile', sections) + + assert.strictEqual(provider.getEndpointUrl(), 'https://process-endpoint.example.com') + }) + + it('handles empty endpoint URL string', async function () { + const ini = ` + [profile test-profile] + aws_access_key_id = test-key + aws_secret_access_key = test-secret + region = us-west-2 + endpoint_url = + ` + const sections = await createTestSections(ini) + const provider = new SharedCredentialsProvider('test-profile', sections) + + assert.strictEqual(provider.getEndpointUrl(), undefined) + }) + + it('endpoint URL does not affect profile validation', async function () { + const ini = ` + [profile valid-profile] + aws_access_key_id = test-key + aws_secret_access_key = test-secret + region = us-west-2 + endpoint_url = https://custom-endpoint.example.com + ` + const sections = await createTestSections(ini) + const provider = new SharedCredentialsProvider('valid-profile', sections) + + assert.strictEqual(provider.validate(), undefined) + assert.strictEqual(await provider.isAvailable(), true) + }) +}) diff --git a/packages/core/src/test/awsService/accessanalyzer/iamPolicyChecks.test.ts b/packages/core/src/test/awsService/accessanalyzer/iamPolicyChecks.test.ts index 995fc1588d6..15f1c398506 100644 --- a/packages/core/src/test/awsService/accessanalyzer/iamPolicyChecks.test.ts +++ b/packages/core/src/test/awsService/accessanalyzer/iamPolicyChecks.test.ts @@ -12,7 +12,7 @@ import { PolicyChecksError, } from '../../../awsService/accessanalyzer/vue/iamPolicyChecks' import { globals } from '../../../shared' -import { AccessAnalyzer, Config } from 'aws-sdk' +import { AccessAnalyzerClient } from '@aws-sdk/client-accessanalyzer' import * as s3Client from '../../../shared/clients/s3' import { S3Client } from '../../../shared/clients/s3' import * as iamPolicyChecks from '../../../awsService/accessanalyzer/vue/iamPolicyChecks' @@ -97,9 +97,8 @@ describe('validatePolicy', function () { let executeCommandStub: sinon.SinonStub let pushValidatePolicyDiagnosticStub: sinon.SinonStub let validateDiagnosticSetStub: sinon.SinonStub - const client = new AccessAnalyzer() - client.config = new Config() - const validatePolicyMock = sinon.mock(AccessAnalyzer) + const client = new AccessAnalyzerClient() + const validatePolicyMock = sinon.mock(client) beforeEach(function () { sandbox = sinon.createSandbox() @@ -317,7 +316,7 @@ describe('customChecks', function () { beforeEach(function () { sandbox = sinon.createSandbox() - const client = AccessAnalyzer.prototype + const client = AccessAnalyzerClient.prototype const initialData = { cfnParameterPath: '', checkAccessNotGrantedActionsTextArea: '', diff --git a/packages/core/src/test/awsService/apigateway/commands/invokeRemoteRestApi.test.ts b/packages/core/src/test/awsService/apigateway/commands/invokeRemoteRestApi.test.ts index c50b3cf7555..26c145dbb8f 100644 --- a/packages/core/src/test/awsService/apigateway/commands/invokeRemoteRestApi.test.ts +++ b/packages/core/src/test/awsService/apigateway/commands/invokeRemoteRestApi.test.ts @@ -5,7 +5,7 @@ import assert from 'assert' import { listValidMethods } from '../../../../awsService/apigateway/vue/invokeRemoteRestApi' -import { Resource } from 'aws-sdk/clients/apigateway' +import { Resource } from '@aws-sdk/client-api-gateway' describe('listValidMethods', function () { const allMethods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'] diff --git a/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts b/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts index f959ef0e572..cf8aaf9adae 100644 --- a/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts +++ b/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts @@ -16,6 +16,7 @@ import { samconfigCompleteData, samconfigInvalidData, validTemplateData, + capacityProviderTemplateData, } from '../../../shared/sam/samTestUtils' import { assertLogsContain } from '../../../globalSetup.test' import { getTestWindow } from '../../../shared/vscode/window' @@ -129,10 +130,13 @@ describe('samProject', () => { assert(lambdaResourceNode) assert(s3BucketResourceNode) // validate Lambda node - assert.strictEqual(lambdaResourceNode.Handler, 'app.lambda_handler') + assert('Handler' in lambdaResourceNode && lambdaResourceNode.Handler === 'app.lambda_handler') assert.strictEqual(lambdaResourceNode.Id, 'ResizerFunction') - assert.strictEqual(lambdaResourceNode.Runtime, 'python3.12') - assert(lambdaResourceNode.Events && lambdaResourceNode.Events.length === 1) + assert('Runtime' in lambdaResourceNode && lambdaResourceNode.Runtime === 'python3.12') + assert( + 'Events' in lambdaResourceNode && lambdaResourceNode.Events && lambdaResourceNode.Events.length === 1 + ) + assert('Events' in lambdaResourceNode && lambdaResourceNode.Events) assert.deepStrictEqual(lambdaResourceNode.Events[0], { Id: 'FileUpload', Type: 'S3', @@ -141,10 +145,10 @@ describe('samProject', () => { }) // validate S3 Bucket assert.strictEqual(s3BucketResourceNode.Id, 'SourceBucket') - assert(!s3BucketResourceNode.Events) - assert(!s3BucketResourceNode.Handler) - assert(!s3BucketResourceNode.Method) - assert(!s3BucketResourceNode.Runtime) + assert(!('Events' in s3BucketResourceNode)) + assert(!('Handler' in s3BucketResourceNode)) + assert(!('Method' in s3BucketResourceNode)) + assert(!('Runtime' in s3BucketResourceNode)) }) it('throws ToolkitError when fails to load Cloudformation template values', async () => { @@ -154,5 +158,33 @@ describe('samProject', () => { new ToolkitError(`Template at ${mockSamAppLocation.samTemplateUri.fsPath} is not valid`) ) }) + + it('parses capacity provider resources correctly', async () => { + await testFolder.write('template.yaml', capacityProviderTemplateData) + const { resourceTree } = await getApp(mockSamAppLocation) + + // Should have 2 resources: capacity provider and function + assert.strictEqual(resourceTree.length, 2) + + const capacityProviderNode = resourceTree.find((node) => node.Type === 'AWS::Serverless::CapacityProvider') + const functionNode = resourceTree.find((node) => node.Type === 'AWS::Serverless::Function') + + // Validate capacity provider node + assert(capacityProviderNode) + assert.strictEqual(capacityProviderNode.Id, 'MyCapacityProvider') + assert('Architectures' in capacityProviderNode && capacityProviderNode.Architectures === 'x86_64') + + // Validate function node has capacity provider config with intrinsic function + assert(functionNode) + assert.strictEqual(functionNode.Id, 'MyFunction') + assert('CapacityProviderConfig' in functionNode && typeof functionNode.CapacityProviderConfig === 'object') + + // Verify the intrinsic function is preserved as an object (not stringified) + const config = functionNode.CapacityProviderConfig as any + assert(config.Arn) + assert.strictEqual(typeof config.Arn, 'object') + assert('Fn::GetAtt' in config.Arn) + assert.deepStrictEqual(config.Arn['Fn::GetAtt'], ['MyCapacityProvider', 'Arn']) + }) }) }) diff --git a/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2sam.test.ts b/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2sam.test.ts index d26d0131d1e..f07343b33a9 100644 --- a/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2sam.test.ts +++ b/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2sam.test.ts @@ -156,6 +156,7 @@ describe('lambda2sam', function () { accessKeyId: 'test-key', secretAccessKey: 'test-secret', }), + endpointUrl: undefined, } sandbox.stub(authUtils, 'getIAMConnection').resolves(mockConnection) diff --git a/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2samCoreLogic.test.ts b/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2samCoreLogic.test.ts index 552d0104b7e..c618dac2197 100644 --- a/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2samCoreLogic.test.ts +++ b/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2samCoreLogic.test.ts @@ -19,7 +19,7 @@ import { ToolkitError } from '../../../../shared/errors' import os from 'os' import path from 'path' import { LAMBDA_FUNCTION_TYPE } from '../../../../shared/cloudformation/cloudformation' -import { ResourcesToImport } from 'aws-sdk/clients/cloudformation' +import { ResourceToImport } from '@aws-sdk/client-cloudformation' describe('lambda2samCoreLogic', function () { let sandbox: sinon.SinonSandbox @@ -416,27 +416,13 @@ describe('lambda2samCoreLogic', function () { describe('deployCfnTemplate', function () { it('deploys a CloudFormation template and returns stack info', async function () { - // Setup CloudFormation template - using 'as any' to bypass strict typing for tests - const template: cloudFormation.Template = { - AWSTemplateFormatVersion: '2010-09-09', - Resources: { - TestFunc: { - Type: cloudFormation.LAMBDA_FUNCTION_TYPE, - Properties: { - FunctionName: 'test-function', - PackageType: 'Zip', - }, - }, - }, - } as any + // Setup CloudFormation template + const template: cloudFormation.Template = mockCloudFormationTemplate() // Setup Lambda node - const lambdaNode = { - name: 'test-function', - regionCode: 'us-west-2', - } as LambdaFunctionNode + const lambdaNode = mockLambdaNode() - const resourceToImport: ResourcesToImport = [ + const resourceToImport: ResourceToImport[] = [ { ResourceType: LAMBDA_FUNCTION_TYPE, LogicalResourceId: 'TestFunc', @@ -475,30 +461,16 @@ describe('lambda2samCoreLogic', function () { }) it('throws an error when change set creation fails', async function () { - // Setup CloudFormation template - using 'as any' to bypass strict typing for tests - const template: cloudFormation.Template = { - AWSTemplateFormatVersion: '2010-09-09', - Resources: { - TestFunc: { - Type: cloudFormation.LAMBDA_FUNCTION_TYPE, - Properties: { - FunctionName: 'test-function', - PackageType: 'Zip', - }, - }, - }, - } as any + // Setup CloudFormation template + const template: cloudFormation.Template = mockCloudFormationTemplate() // Setup Lambda node - const lambdaNode = { - name: 'test-function', - regionCode: 'us-west-2', - } as LambdaFunctionNode + const lambdaNode = mockLambdaNode() // Make createChangeSet fail cfnClientStub.createChangeSet.resolves({}) // No Id - const resourceToImport: ResourcesToImport = [ + const resourceToImport: ResourceToImport[] = [ { ResourceType: LAMBDA_FUNCTION_TYPE, LogicalResourceId: 'TestFunc', @@ -522,32 +494,14 @@ describe('lambda2samCoreLogic', function () { describe('callExternalApiForCfnTemplate', function () { it('extracts function name from ARN in ResourceIdentifier', async function () { // Setup Lambda node - const lambdaNode = { - name: 'test-function', - regionCode: 'us-east-2', - arn: 'arn:aws:lambda:us-east-2:123456789012:function:test-function', - } as LambdaFunctionNode + const lambdaNode = mockLambdaNode(true) // Mock IAM connection - const mockConnection = { - type: 'iam' as const, - id: 'test-connection', - label: 'Test Connection', - state: 'valid' as const, - getCredentials: sandbox.stub().resolves({ - accessKeyId: 'test-key', - secretAccessKey: 'test-secret', - }), - } + const mockConnection = mockIamConnection() sandbox.stub(authUtils, 'getIAMConnection').resolves(mockConnection) // Mock fetch response - const mockFetch = sandbox.stub(global, 'fetch').resolves({ - ok: true, - json: sandbox.stub().resolves({ - cloudFormationTemplateId: 'test-template-id', - }), - } as any) + const mockFetch = mockFetchResponse(sandbox) // Setup CloudFormation client to return ARN in ResourceIdentifier cfnClientStub.describeGeneratedTemplate.resolves({ @@ -580,32 +534,14 @@ describe('lambda2samCoreLogic', function () { it('preserves function name when not an ARN', async function () { // Setup Lambda node - const lambdaNode = { - name: 'test-function', - regionCode: 'us-east-2', - arn: 'arn:aws:lambda:us-east-2:123456789012:function:test-function', - } as LambdaFunctionNode + const lambdaNode = mockLambdaNode(true) // Mock IAM connection - const mockConnection = { - type: 'iam' as const, - id: 'test-connection', - label: 'Test Connection', - state: 'valid' as const, - getCredentials: sandbox.stub().resolves({ - accessKeyId: 'test-key', - secretAccessKey: 'test-secret', - }), - } + const mockConnection = mockIamConnection() sandbox.stub(authUtils, 'getIAMConnection').resolves(mockConnection) // Mock fetch response - sandbox.stub(global, 'fetch').resolves({ - ok: true, - json: sandbox.stub().resolves({ - cloudFormationTemplateId: 'test-template-id', - }), - } as any) + mockFetchResponse(sandbox) // Setup CloudFormation client to return plain function name cfnClientStub.describeGeneratedTemplate.resolves({ @@ -631,32 +567,14 @@ describe('lambda2samCoreLogic', function () { it('handles non-Lambda resources without modification', async function () { // Setup Lambda node - const lambdaNode = { - name: 'test-function', - regionCode: 'us-east-2', - arn: 'arn:aws:lambda:us-east-2:123456789012:function:test-function', - } as LambdaFunctionNode + const lambdaNode = mockLambdaNode(true) // Mock IAM connection - const mockConnection = { - type: 'iam' as const, - id: 'test-connection', - label: 'Test Connection', - state: 'valid' as const, - getCredentials: sandbox.stub().resolves({ - accessKeyId: 'test-key', - secretAccessKey: 'test-secret', - }), - } + const mockConnection = mockIamConnection() sandbox.stub(authUtils, 'getIAMConnection').resolves(mockConnection) // Mock fetch response - sandbox.stub(global, 'fetch').resolves({ - ok: true, - json: sandbox.stub().resolves({ - cloudFormationTemplateId: 'test-template-id', - }), - } as any) + mockFetchResponse(sandbox) // Setup CloudFormation client to return mixed resource types cfnClientStub.describeGeneratedTemplate.resolves({ @@ -696,10 +614,7 @@ describe('lambda2samCoreLogic', function () { describe('lambdaToSam', function () { it('converts a Lambda function to a SAM project', async function () { // Setup Lambda node - const lambdaNode = { - name: 'test-function', - regionCode: 'us-west-2', - } as LambdaFunctionNode + const lambdaNode = mockLambdaNode() // Setup AWS Lambda client responses lambdaClientStub.getFunction.resolves({ @@ -781,4 +696,59 @@ describe('lambda2samCoreLogic', function () { ) }) }) + + function mockLambdaNode(withArn: boolean = false) { + if (withArn) { + return { + name: 'test-function', + regionCode: 'us-east-2', + arn: 'arn:aws:lambda:us-east-2:123456789012:function:test-function', + } as LambdaFunctionNode + } else { + return { + name: 'test-function', + regionCode: 'us-east-2', + } as LambdaFunctionNode + } + } + + function mockIamConnection() { + return { + type: 'iam' as const, + id: 'test-connection', + label: 'Test Connection', + state: 'valid' as const, + getCredentials: sandbox.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + endpointUrl: undefined, + } + } + + function mockCloudFormationTemplate(): cloudFormation.Template { + return { + AWSTemplateFormatVersion: '2010-09-09', + Resources: { + TestFunc: { + Type: cloudFormation.LAMBDA_FUNCTION_TYPE, + Properties: { + FunctionName: 'test-function', + PackageType: 'Zip', + Handler: 'index.handler', + CodeUri: 's3://test-bucket/test-key', + }, + }, + }, + } + } + + function mockFetchResponse(sandbox: sinon.SinonSandbox) { + return sandbox.stub(global, 'fetch').resolves({ + ok: true, + json: sandbox.stub().resolves({ + cloudFormationTemplateId: 'test-template-id', + }), + } as any) + } }) diff --git a/packages/core/src/test/awsService/appBuilder/serverlessLand/wizard.test.ts b/packages/core/src/test/awsService/appBuilder/serverlessLand/wizard.test.ts index e8a51238d6f..cb7117736cf 100644 --- a/packages/core/src/test/awsService/appBuilder/serverlessLand/wizard.test.ts +++ b/packages/core/src/test/awsService/appBuilder/serverlessLand/wizard.test.ts @@ -88,7 +88,10 @@ describe('CreateWizard', async () => { assert.ok(resourceNodes[0] instanceof ResourceNode) const lambdaResource = resourceNodes[2] as ResourceNode - assert.strictEqual(lambdaResource.resource.resource.Runtime, 'python3.12') + assert( + 'Runtime' in lambdaResource.resource.resource && + lambdaResource.resource.resource.Runtime === 'python3.14' + ) prompterTester.assertCallAll() }) diff --git a/packages/core/src/test/awsService/appBuilder/utils.test.ts b/packages/core/src/test/awsService/appBuilder/utils.test.ts index eaaa69254d7..36c75d2c6e5 100644 --- a/packages/core/src/test/awsService/appBuilder/utils.test.ts +++ b/packages/core/src/test/awsService/appBuilder/utils.test.ts @@ -12,6 +12,7 @@ import fs from '../../../shared/fs/fs' import { ResourceNode } from '../../../awsService/appBuilder/explorer/nodes/resourceNode' import path from 'path' import { SERVERLESS_FUNCTION_TYPE } from '../../../shared/cloudformation/cloudformation' +import { FunctionResourceEntity } from '../../../awsService/appBuilder/explorer/samProject' import { runOpenHandler, runOpenTemplate, @@ -26,9 +27,11 @@ import { assertTextEditorContains } from '../../testUtil' import { DefaultLambdaClient } from '../../../shared/clients/lambdaClient' import { ToolkitError } from '../../../shared/errors' import globals from '../../../shared/extensionGlobals' +import { Runtime } from '@aws-sdk/client-lambda' +import { CloudFormationClient } from '@aws-sdk/client-cloudformation' interface TestScenario { - runtime: string + runtime: Runtime handler: string codeUri: string fileLocation: string @@ -151,7 +154,7 @@ describe('AppBuilder Utils', function () { Runtime: scenario.runtime, Handler: scenario.handler, CodeUri: scenario.codeUri, - } + } as FunctionResourceEntity ) await fs.mkdir(path.join(tempFolder, ...path.dirname(scenario.fileLocation).split('/'))) await fs.writeFile(path.join(tempFolder, ...scenario.fileLocation.split('/')), scenario.fileInfo) @@ -197,7 +200,7 @@ describe('AppBuilder Utils', function () { Runtime: scenario.runtime, Handler: scenario.handler, CodeUri: scenario.codeUri, - } + } as FunctionResourceEntity ) await fs.mkdir(path.join(tempFolder, ...path.dirname(scenario.fileLocation).split('/'))) await fs.writeFile(path.join(tempFolder, ...scenario.fileLocation.split('/')), scenario.fileInfo) @@ -224,7 +227,7 @@ describe('AppBuilder Utils', function () { Runtime: 'java21', Handler: 'resizer.App::handleRequest', CodeUri: 'ResizerFunction', - } + } as FunctionResourceEntity ) // When 2 java handler with right name under code URI await fs.mkdir( @@ -505,7 +508,7 @@ describe('AppBuilder Utils', function () { mockLambdaClient.invoke.rejects(permissionError) try { - await enhancedClient.invoke('test-function', '{}') + await enhancedClient.invoke('test-function', new TextEncoder().encode('{}')) assert.fail('Expected error to be thrown') } catch (error) { assert(error instanceof ToolkitError) @@ -571,19 +574,13 @@ describe('AppBuilder Utils', function () { }) describe('EnhancedCloudFormationClient', function () { - let mockCfnClient: any + let mockCfnClient: sinon.SinonStubbedInstance let enhancedClient: EnhancedCloudFormationClient beforeEach(function () { // Create a mock CloudFormation client with all required methods - mockCfnClient = { - describeStacks: sandbox.stub(), - getTemplate: sandbox.stub(), - createChangeSet: sandbox.stub(), - describeStackResource: sandbox.stub(), - describeStackResources: sandbox.stub(), - } - enhancedClient = new EnhancedCloudFormationClient(mockCfnClient, 'us-east-1') + mockCfnClient = sandbox.createStubInstance(CloudFormationClient) + enhancedClient = new EnhancedCloudFormationClient(mockCfnClient as any, 'us-east-1') }) it('should enhance permission errors for describeStacks', async function () { @@ -592,9 +589,7 @@ describe('AppBuilder Utils', function () { time: new Date(), statusCode: 403, }) - mockCfnClient.describeStacks.returns({ - promise: sandbox.stub().rejects(permissionError), - } as any) + mockCfnClient.send.rejects(permissionError) try { await enhancedClient.describeStacks({ StackName: 'test-stack' }) @@ -619,9 +614,7 @@ describe('AppBuilder Utils', function () { time: new Date(), statusCode: 403, }) - mockCfnClient.getTemplate.returns({ - promise: sandbox.stub().rejects(permissionError), - } as any) + mockCfnClient.send.rejects(permissionError) try { await enhancedClient.getTemplate({ StackName: 'test-stack' }) @@ -644,9 +637,7 @@ describe('AppBuilder Utils', function () { time: new Date(), statusCode: 403, }) - mockCfnClient.createChangeSet.returns({ - promise: sandbox.stub().rejects(permissionError), - } as any) + mockCfnClient.send.rejects(permissionError) try { await enhancedClient.createChangeSet({ @@ -673,9 +664,7 @@ describe('AppBuilder Utils', function () { time: new Date(), statusCode: 403, }) - mockCfnClient.describeStackResource.returns({ - promise: sandbox.stub().rejects(permissionError), - } as any) + mockCfnClient.send.rejects(permissionError) try { await enhancedClient.describeStackResource({ @@ -701,9 +690,7 @@ describe('AppBuilder Utils', function () { time: new Date(), statusCode: 403, }) - mockCfnClient.describeStackResources.returns({ - promise: sandbox.stub().rejects(permissionError), - } as any) + mockCfnClient.send.rejects(permissionError) try { await enhancedClient.describeStackResources({ StackName: 'test-stack' }) @@ -722,9 +709,7 @@ describe('AppBuilder Utils', function () { it('should pass through non-permission errors', async function () { const nonPermissionError = new Error('Stack not found') - mockCfnClient.describeStacks.returns({ - promise: sandbox.stub().rejects(nonPermissionError), - } as any) + mockCfnClient.send.rejects(nonPermissionError) try { await enhancedClient.describeStacks({ StackName: 'test-stack' }) @@ -736,9 +721,7 @@ describe('AppBuilder Utils', function () { it('should return successful results when no errors occur', async function () { const mockResponse = { Stacks: [{ StackName: 'test-stack' }] } - mockCfnClient.describeStacks.returns({ - promise: sandbox.stub().resolves(mockResponse), - } as any) + mockCfnClient.send.resolves(mockResponse) const result = await enhancedClient.describeStacks({ StackName: 'test-stack' }) assert.strictEqual(result, mockResponse) @@ -748,7 +731,7 @@ describe('AppBuilder Utils', function () { describe('Client Factory Functions', function () { beforeEach(function () { // Stub the global SDK client builder - sandbox.stub(globals.sdkClientBuilder, 'createAwsService').resolves({} as any) + sandbox.stub(globals.sdkClientBuilderV3, 'createAwsService').resolves({} as any) }) it('should return EnhancedLambdaClient from getLambdaClient', function () { diff --git a/packages/core/src/test/awsService/appBuilder/walkthrough.test.ts b/packages/core/src/test/awsService/appBuilder/walkthrough.test.ts index 44a31b3cae9..add3ee6e546 100644 --- a/packages/core/src/test/awsService/appBuilder/walkthrough.test.ts +++ b/packages/core/src/test/awsService/appBuilder/walkthrough.test.ts @@ -15,6 +15,7 @@ import { RuntimeLocationWizard, genWalkthroughProject, openProjectInWorkspace, + installLocalStackExtension, } from '../../../awsService/appBuilder/walkthrough' import { createWizardTester } from '../../shared/wizards/wizardTestUtils' import { fs } from '../../../shared' @@ -25,6 +26,7 @@ import { ChildProcess } from '../../../shared/utilities/processUtils' import { assertTelemetryCurried } from '../../testUtil' import { HttpResourceFetcher } from '../../../shared/resourcefetcher/node/httpResourceFetcher' import { SamCliInfoInvocation } from '../../../shared/sam/cli/samCliInfo' +import type { ToolId } from '../../../shared/telemetry/telemetry' import { CodeScansState } from '../../../codewhisperer' interface TestScenario { @@ -49,6 +51,11 @@ const scenarios: TestScenario[] = [ platform: 'win32', shouldSucceed: true, }, + { + toolID: 'finch', + platform: 'win32', + shouldSucceed: false, + }, { toolID: 'aws-cli', platform: 'darwin', @@ -64,6 +71,11 @@ const scenarios: TestScenario[] = [ platform: 'darwin', shouldSucceed: true, }, + { + toolID: 'finch', + platform: 'darwin', + shouldSucceed: true, + }, { toolID: 'aws-cli', platform: 'linux', @@ -79,6 +91,11 @@ const scenarios: TestScenario[] = [ platform: 'linux', shouldSucceed: false, }, + { + toolID: 'finch', + platform: 'linux', + shouldSucceed: false, + }, ] describe('AppBuilder Walkthrough', function () { @@ -207,11 +224,14 @@ describe('AppBuilder Walkthrough', function () { }) it('download serverlessland proj', async function () { + const config = vscode.workspace.getConfiguration('aws.samcli') + await config.update('enableCodeLenses', false, vscode.ConfigurationTarget.Global) // When await genWalkthroughProject('API', workspaceUri, 'python') // Then template should be overwritten assert.equal(await fs.exists(vscode.Uri.joinPath(workspaceUri, 'template.yaml')), true) assert.notEqual(await fs.readFileText(vscode.Uri.joinPath(workspaceUri, 'template.yaml')), prevInfo) + await config.update('enableCodeLenses', true, vscode.ConfigurationTarget.Global) }) }) @@ -460,5 +480,97 @@ describe('AppBuilder Walkthrough', function () { toolId: 'sam-cli', }) }) + + describe('Install LocalStack Extension', function () { + // @ts-ignore until TODO from src/awsService/appBuilder/walkthrough.ts:installLocalStackExtension + const expectedLocalStackToolId: ToolId = 'localstack' + + it('should show already installed message when extension exists', async function () { + const mockExtension = { id: 'localstack.localstack' } + sandbox + .stub(vscode.extensions, 'getExtension') + .withArgs('localstack.localstack') + .returns(mockExtension as any) + const spyExecuteCommand = sandbox.spy(vscode.commands, 'executeCommand') + + await installLocalStackExtension('test-source') + + const message = await getTestWindow().waitForMessage(/LocalStack extension is already installed/) + message.close() + + // Verify installation command was not called + sandbox.assert.neverCalledWith(spyExecuteCommand, 'workbench.extensions.installExtension') + + // Verify telemetry + assertTelemetry({ + result: 'Succeeded', + source: 'test-source', + toolId: expectedLocalStackToolId, + }) + }) + + it('should successfully install extension when not present', async function () { + sandbox.stub(vscode.extensions, 'getExtension').withArgs('localstack.localstack').returns(undefined) + const spyExecuteCommand = sandbox.stub(vscode.commands, 'executeCommand').resolves() + + await installLocalStackExtension('test-source') + + const message = await getTestWindow().waitForMessage(/LocalStack extension has been installed/) + message.close() + + // Verify installation command was called with correct extension ID + sandbox.assert.calledWith( + spyExecuteCommand, + 'workbench.extensions.installExtension', + 'localstack.localstack' + ) + + // Verify telemetry + assertTelemetry({ + result: 'Succeeded', + source: 'test-source', + toolId: expectedLocalStackToolId, + }) + }) + + it('should handle installation failure and throw ToolkitError', async function () { + sandbox.stub(vscode.extensions, 'getExtension').withArgs('localstack.localstack').returns(undefined) + const installError = new Error('Installation failed') + sandbox.stub(vscode.commands, 'executeCommand').rejects(installError) + + await assert.rejects(installLocalStackExtension('test-source'), (error: any) => { + assert.strictEqual(error.message, 'Failed to install LocalStack extension') + assert.strictEqual(error.cause, installError) + return true + }) + + // Verify telemetry is still recorded even on failure + assertTelemetry({ + result: 'Failed', + source: 'test-source', + toolId: expectedLocalStackToolId, + }) + }) + + it('should record telemetry with correct source parameter', async function () { + const mockExtension = { id: 'localstack.localstack' } + sandbox + .stub(vscode.extensions, 'getExtension') + .withArgs('localstack.localstack') + .returns(mockExtension as any) + + await installLocalStackExtension('walkthrough-button') + + const message = await getTestWindow().waitForMessage(/LocalStack extension is already installed/) + message.close() + + // Verify telemetry includes the correct source + assertTelemetry({ + result: 'Succeeded', + source: 'walkthrough-button', + toolId: expectedLocalStackToolId, + }) + }) + }) }) }) diff --git a/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts index 221d0a6fee3..3457623cb79 100644 --- a/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts +++ b/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts @@ -21,7 +21,7 @@ import { import { Settings } from '../../../../shared/settings' import { LogDataCodeLensProvider } from '../../../../awsService/cloudWatchLogs/document/logDataCodeLensProvider' import { CLOUDWATCH_LOGS_SCHEME } from '../../../../shared/constants' -import { FilteredLogEvent } from 'aws-sdk/clients/cloudwatchlogs' +import { FilteredLogEvent } from '@aws-sdk/client-cloudwatch-logs' const getLogEventsMessage = 'This is from getLogEvents' diff --git a/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts index 6f65bc2438c..1a00bb49c69 100644 --- a/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts +++ b/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts @@ -25,7 +25,7 @@ import { testLogData, unregisteredData, } from '../utils.test' -import { FilteredLogEvents } from 'aws-sdk/clients/cloudwatchlogs' +import { FilteredLogEvent } from '@aws-sdk/client-cloudwatch-logs' import { formatDateTimestamp } from '../../../../shared/datetime' describe('LogDataRegistry', async function () { @@ -128,8 +128,8 @@ describe('LogDataRegistry', async function () { const pageToken1 = 'page1Token' const pageToken2 = 'page2Token' - function createCwlEvents(id: string, count: number): FilteredLogEvents { - let events: FilteredLogEvents = [] + function createCwlEvents(id: string, count: number): FilteredLogEvent[] { + let events: FilteredLogEvent[] = [] for (let i = 0; i < count; i++) { events = events.concat({ message: `message-${id}`, logStreamName: `stream-${id}` }) } diff --git a/packages/core/src/test/awsService/cloudformation/auth/credentials.test.ts b/packages/core/src/test/awsService/cloudformation/auth/credentials.test.ts new file mode 100644 index 00000000000..138c5812288 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/auth/credentials.test.ts @@ -0,0 +1,92 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import * as jose from 'jose' +import { AwsCredentialsService, encryptionKey } from '../../../../awsService/cloudformation/auth/credentials' + +describe('AwsCredentialsService', function () { + let sandbox: sinon.SinonSandbox + let mockStacksManager: any + let mockResourcesManager: any + let mockRegionManager: any + let mockClient: any + let credentialsService: AwsCredentialsService + + beforeEach(function () { + sandbox = sinon.createSandbox() + mockStacksManager = { reload: sandbox.stub(), hasMore: sandbox.stub().returns(false), clear: sandbox.stub() } + mockResourcesManager = { reload: sandbox.stub() } + mockClient = { sendRequest: sandbox.stub() } + + const mockRegionManager = { getSelectedRegion: () => 'us-east-1' } as any + + credentialsService = new AwsCredentialsService(mockStacksManager, mockResourcesManager, mockRegionManager) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('createEncryptedCredentialsRequest', function () { + beforeEach(function () { + credentialsService = new AwsCredentialsService(mockStacksManager, mockResourcesManager, mockRegionManager) + }) + + it('should create encrypted request with correct structure', async function () { + const mockCredentials = { + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + expiration: new Date(), + } + + const result = await (credentialsService as any).createEncryptedCredentialsRequest(mockCredentials) + + assert.strictEqual(typeof result.data, 'string') + assert.strictEqual(result.encrypted, true) + }) + + it('should encrypt credentials that can be decrypted', async function () { + const mockCredentials = { + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + expiration: new Date(), + } + + const encryptedRequest = await (credentialsService as any).createEncryptedCredentialsRequest( + mockCredentials + ) + + // Verify we can decrypt it back + const decrypted = await jose.compactDecrypt(encryptedRequest.data, encryptionKey) + const decryptedData = JSON.parse(new TextDecoder().decode(decrypted.plaintext)) + + // Compare with expected serialized format (Date becomes string in JSON) + const expectedCredentials = { + ...mockCredentials, + expiration: mockCredentials.expiration.toISOString(), + } + assert.deepStrictEqual(decryptedData.data, expectedCredentials) + }) + }) + + describe('initialize', function () { + it('should accept language client', async function () { + credentialsService = new AwsCredentialsService(mockStacksManager, mockResourcesManager, mockRegionManager) + await credentialsService.initialize(mockClient) + // Test passes if no error thrown + assert(true) + }) + + it('should clear stacks', async function () { + credentialsService = new AwsCredentialsService(mockStacksManager, mockResourcesManager, mockRegionManager) + await credentialsService.initialize(mockClient) + assert(mockStacksManager.clear.calledOnce) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/cfn-init/cfnEnvironmentManager.test.ts b/packages/core/src/test/awsService/cloudformation/cfn-init/cfnEnvironmentManager.test.ts new file mode 100644 index 00000000000..0bc4c5098f5 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/cfn-init/cfnEnvironmentManager.test.ts @@ -0,0 +1,471 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { strict as assert } from 'assert' +import * as sinon from 'sinon' +import { CfnEnvironmentManager } from '../../../../awsService/cloudformation/cfn-init/cfnEnvironmentManager' +import { Auth } from '../../../../auth/auth' +import { Connection } from '../../../../auth/connection' +import { globals } from '../../../../shared' +import { workspace, commands } from 'vscode' +import fs from '../../../../shared/fs/fs' +import { CfnEnvironmentSelector } from '../../../../awsService/cloudformation/ui/cfnEnvironmentSelector' +import { CfnEnvironmentFileSelector } from '../../../../awsService/cloudformation/ui/cfnEnvironmentFileSelector' +import { OnStackFailure } from '@aws-sdk/client-cloudformation' +import * as environmentApi from '../../../../awsService/cloudformation/cfn-init/cfnEnvironmentApi' +import { getTestWindow } from '../../../shared/vscode/window' + +describe('CfnEnvironmentManager', () => { + let environmentManager: CfnEnvironmentManager + let mockAuth: sinon.SinonStubbedInstance + let mockWorkspaceState: any + let mockEnvironmentSelector: sinon.SinonStubbedInstance + let mockEnvironmentFileSelector: sinon.SinonStubbedInstance + let fsStub: sinon.SinonStub + let workspaceStub: sinon.SinonStub + let parseEnvironmentFilesStub: sinon.SinonStub + let mockClient: any + + let existsDirStub: sinon.SinonStub + let existsFileStub: sinon.SinonStub + + beforeEach(() => { + mockAuth = { + getConnection: sinon.stub(), + useConnection: sinon.stub(), + activeConnection: { + id: 'profile:test-profile', + type: 'iam', + label: 'test-profile', + state: 'valid', + } as unknown as Connection, + } as sinon.SinonStubbedInstance + + sinon.stub(Auth, 'instance').get(() => mockAuth) + + mockWorkspaceState = { + get: sinon.stub(), + update: sinon.stub(), + } + sinon.stub(globals, 'context').value({ workspaceState: mockWorkspaceState }) + + mockEnvironmentSelector = { + selectEnvironment: sinon.stub(), + } as unknown as sinon.SinonStubbedInstance + + mockEnvironmentFileSelector = { + selectEnvironmentFile: sinon.stub(), + } as unknown as sinon.SinonStubbedInstance + + fsStub = sinon.stub(fs, 'readFileText') + // Mock project as initialized by default + existsDirStub = sinon.stub(fs, 'existsDir').resolves(true) + existsFileStub = sinon.stub(fs, 'existsFile').resolves(true) + + workspaceStub = sinon.stub(workspace, 'workspaceFolders').value([{ uri: { fsPath: '/test/workspace' } }]) + parseEnvironmentFilesStub = sinon.stub(environmentApi, 'parseCfnEnvironmentFiles') + mockClient = {} + + environmentManager = new CfnEnvironmentManager(mockClient, mockEnvironmentSelector, mockEnvironmentFileSelector) + }) + + afterEach(() => { + sinon.restore() + }) + + describe('getSelectedEnvironmentName', () => { + it('should return selected environment from workspace state', () => { + mockWorkspaceState.get.returns('test-env') + + const result = environmentManager.getSelectedEnvironmentName() + + assert.strictEqual(result, 'test-env') + assert(mockWorkspaceState.get.calledWith('aws.cloudformation.selectedEnvironment')) + }) + }) + + describe('promptInitializeIfNeeded', () => { + it('should return false when project is already initialized', async () => { + // Project is initialized by default in beforeEach + const result = await environmentManager.promptInitializeIfNeeded('Test Operation') + + assert.strictEqual(result, false) + const messages = getTestWindow().shownMessages + assert.strictEqual(messages.length, 0) + }) + + it('should show warning and execute command when user clicks Initialize Project', async () => { + existsDirStub.resolves(false) + existsFileStub.resolves(false) + + getTestWindow().onDidShowMessage((message) => { + if (message.message === 'You must initialize your CFN Project to perform Test Operation') { + message.selectItem('Initialize Project') + } + }) + + const executeCommandStub = sinon.stub(commands, 'executeCommand') + + const result = await environmentManager.promptInitializeIfNeeded('Test Operation') + + assert.strictEqual(result, true) + const messages = getTestWindow().shownMessages + assert(messages.some((m) => m.message === 'You must initialize your CFN Project to perform Test Operation')) + assert(executeCommandStub.calledWith('aws.cloudformation.init.initializeProject')) + }) + }) + + describe('selectEnvironment', () => { + it('should show warning when project is not initialized', async () => { + // Override default - mock project as not initialized + existsDirStub.resolves(false) + existsFileStub.resolves(false) + + // Set up message handler to simulate user clicking "Initialize Project" + getTestWindow().onDidShowMessage((message) => { + if (message.message === 'You must initialize your CFN Project to perform Environment Selection') { + // Simulate user clicking the "Initialize Project" button + message.selectItem('Initialize Project') + } + }) + + const executeCommandStub = sinon.stub(commands, 'executeCommand') + + await environmentManager.selectEnvironment() + + const messages = getTestWindow().shownMessages + assert( + messages.some( + (m) => m.message === 'You must initialize your CFN Project to perform Environment Selection' + ) + ) + assert(executeCommandStub.calledWith('aws.cloudformation.init.initializeProject')) + assert(mockEnvironmentSelector.selectEnvironment.notCalled) + }) + + it('should select environment successfully', async () => { + const mockEnvironmentLookup = { 'test-env': { name: 'test-env', profile: 'test-profile' } } + fsStub.resolves(JSON.stringify({ environments: mockEnvironmentLookup })) + mockEnvironmentSelector.selectEnvironment.resolves('test-env') + + const mockConnection = { + id: 'profile:test-profile', + type: 'iam', + label: 'test-profile', + state: 'valid', + } as unknown as Connection + mockAuth.getConnection.resolves(mockConnection) + + const listener = sinon.stub() + environmentManager.addListener(listener) + + await environmentManager.selectEnvironment() + + assert(mockEnvironmentSelector.selectEnvironment.calledWith(mockEnvironmentLookup)) + assert(mockWorkspaceState.update.calledWith('aws.cloudformation.selectedEnvironment', 'test-env')) + assert(mockAuth.getConnection.calledWith({ id: 'profile:test-profile' })) + assert(mockAuth.useConnection.calledWith(mockConnection)) + assert(listener.called) + }) + + it('should handle fetch error gracefully', async () => { + fsStub.rejects(new Error('File not found')) + + await environmentManager.selectEnvironment() + + assert(mockEnvironmentSelector.selectEnvironment.notCalled) + }) + + it('should handle no environment selected', async () => { + const mockEnvironmentLookup = { 'test-env': { name: 'test-env', profile: 'test-profile' } } + fsStub.resolves(JSON.stringify({ environments: mockEnvironmentLookup })) + mockEnvironmentSelector.selectEnvironment.resolves(undefined) + + await environmentManager.selectEnvironment() + + assert(mockWorkspaceState.update.notCalled) + assert(mockAuth.getConnection.notCalled) + }) + + it('should handle missing connection gracefully', async () => { + const mockEnvironmentLookup = { 'test-env': { name: 'test-env', profile: 'missing-profile' } } + fsStub.resolves(JSON.stringify({ environments: mockEnvironmentLookup })) + mockEnvironmentSelector.selectEnvironment.resolves('test-env') + mockAuth.getConnection.resolves(undefined) + + await environmentManager.selectEnvironment() + + assert(mockWorkspaceState.update.calledWith('aws.cloudformation.selectedEnvironment', 'test-env')) + assert(mockAuth.useConnection.notCalled) + }) + }) + + describe('fetchAvailableEnvironments', () => { + it('should fetch environments successfully', async () => { + const mockEnvironmentLookup = { env1: { name: 'env1', profile: 'profile1' } } + fsStub.resolves(JSON.stringify({ environments: mockEnvironmentLookup })) + + const result = await environmentManager.fetchAvailableEnvironments() + + assert.deepStrictEqual(result, mockEnvironmentLookup) + }) + + it('should throw error when workspace not found', async () => { + workspaceStub.value(undefined) + + await assert.rejects( + environmentManager.fetchAvailableEnvironments(), + /You must open a workspace to use CFN environment commands/ + ) + }) + + it('should throw error when file read fails', async () => { + fsStub.rejects(new Error('File not found')) + + await assert.rejects(environmentManager.fetchAvailableEnvironments(), /File not found/) + }) + }) + + describe('selectEnvironmentFile', () => { + let readdirStub: sinon.SinonStub + + beforeEach(() => { + readdirStub = sinon.stub(fs, 'readdir') + }) + + it('should return undefined when no environment selected', async () => { + mockWorkspaceState.get.returns(undefined) + + const result = await environmentManager.selectEnvironmentFile('template.yaml', [{ name: 'Param1' }]) + + assert.strictEqual(result, undefined) + }) + + it('should collect all environment files and pass to selector', async () => { + mockWorkspaceState.get.returns('test-env') + + // Mock multiple files + readdirStub.resolves([ + ['params1.json', 1], + ['params2.yaml', 1], + ['params3.yml', 1], + ]) + + // Mock file contents + fsStub.onCall(0).resolves( + JSON.stringify({ + parameters: { Param1: 'value1' }, + tags: { Tag1: 'value1' }, + 'on-stack-failure': OnStackFailure.DO_NOTHING, + 'import-existing-resources': true, + 'include-nested-stacks': false, + }) + ) + fsStub.onCall(1).resolves('template-file-path: template.yaml\nparameters:\n Param2: value2') + fsStub.onCall(2).resolves('template-file-path: wrong-file.yaml\nparameters:\n Param3: value3') + + // Mock parseEnvironmentFiles response + parseEnvironmentFilesStub.resolves([ + { + fileName: 'params1.json', + deploymentConfig: { + parameters: { Param1: 'value1' }, + tags: { Tag1: 'value1' }, + onStackFailure: OnStackFailure.DO_NOTHING, + importExistingResources: true, + includeNestedStacks: false, + }, + }, + { + fileName: 'params2.yaml', + deploymentConfig: { + templateFilePath: 'template.yaml', + parameters: { Param2: 'value2' }, + }, + }, + { + fileName: 'params3.yml', + deploymentConfig: { + templateFilePath: 'wrong-file.yaml', + parameters: { Param3: 'value3' }, + }, + }, + ]) + + // Mock workspace.asRelativePath to return matching path for template.yaml + sinon.stub(workspace, 'asRelativePath').returns('template.yaml') + + const mockSelectorItem = { + fileName: 'selected.json', + hasMatchingTemplatePath: true, + compatibleParameters: [{ ParameterKey: 'Param1', ParameterValue: 'value1' }], + } + mockEnvironmentFileSelector.selectEnvironmentFile.resolves(mockSelectorItem) + + const result = await environmentManager.selectEnvironmentFile('template.yaml', [ + { name: 'Param1' }, + { name: 'Param2' }, + { name: 'Param3' }, + ]) + + const [selectorItems, paramCount] = mockEnvironmentFileSelector.selectEnvironmentFile.getCall(0).args + + // Assert call arguments + assert(mockEnvironmentFileSelector.selectEnvironmentFile.calledOnce) + assert.strictEqual(selectorItems.length, 3) + assert.strictEqual(paramCount, 3) + + // Check params1.json + assert.strictEqual(selectorItems[0].fileName, 'params1.json') + assert.strictEqual(selectorItems[0].hasMatchingTemplatePath, false) + assert.deepStrictEqual(selectorItems[0].compatibleParameters, [ + { ParameterKey: 'Param1', ParameterValue: 'value1' }, + ]) + assert.deepStrictEqual(selectorItems[0].optionalFlags?.tags, [{ Key: 'Tag1', Value: 'value1' }]) + assert.deepStrictEqual(selectorItems[0].optionalFlags?.includeNestedStacks, false), + assert.deepStrictEqual(selectorItems[0].optionalFlags?.importExistingResources, true), + assert.deepStrictEqual(selectorItems[0].optionalFlags?.onStackFailure, OnStackFailure.DO_NOTHING), + // Check params2.yaml + assert.strictEqual(selectorItems[1].fileName, 'params2.yaml') + assert.strictEqual(selectorItems[1].hasMatchingTemplatePath, true) + assert.deepStrictEqual(selectorItems[1].compatibleParameters, [ + { ParameterKey: 'Param2', ParameterValue: 'value2' }, + ]) + + // Check params3.yml + assert.strictEqual(selectorItems[2].fileName, 'params3.yml') + assert.strictEqual(selectorItems[2].hasMatchingTemplatePath, false) + assert.deepStrictEqual(selectorItems[2].compatibleParameters, [ + { ParameterKey: 'Param3', ParameterValue: 'value3' }, + ]) + assert.strictEqual(result, mockSelectorItem) + }) + + it('should only use files returned from parser', async () => { + mockWorkspaceState.get.returns('test-env') + readdirStub.resolves([ + ['valid1.json', 1], + ['malformed1.json', 1], + ['valid2.yaml', 1], + ['malformed2.yaml', 1], + ['malformed3.yml', 1], + ]) + + // Mock file contents for all 5 files + fsStub.onCall(0).resolves(JSON.stringify({ parameters: { Param1: 'value1' } })) + fsStub.onCall(1).resolves('invalid json') + fsStub.onCall(2).resolves('parameters:\n Param2: value2') + fsStub.onCall(3).resolves('invalid: yaml: content') + fsStub.onCall(4).resolves('null') + + // Parser only returns 2 valid files out of 5 + parseEnvironmentFilesStub.resolves([ + { + fileName: 'valid1.json', + deploymentConfig: { + parameters: { Param1: 'value1' }, + }, + }, + { + fileName: 'valid2.yaml', + deploymentConfig: { + parameters: { Param2: 'value2' }, + }, + }, + ]) + + const mockSelectorItem = { fileName: 'selected.json' } + mockEnvironmentFileSelector.selectEnvironmentFile.resolves(mockSelectorItem) + + await environmentManager.selectEnvironmentFile('template.yaml', [{ name: 'Param1' }, { name: 'Param2' }]) + + // Verify parseEnvironmentFiles was called with all files + assert( + parseEnvironmentFilesStub.calledOnceWith(mockClient, { + documents: [ + { fileName: 'valid1.json', type: 'JSON', content: '{"parameters":{"Param1":"value1"}}' }, + { fileName: 'malformed1.json', type: 'JSON', content: 'invalid json' }, + { fileName: 'valid2.yaml', type: 'YAML', content: 'parameters:\n Param2: value2' }, + { fileName: 'malformed2.yaml', type: 'YAML', content: 'invalid: yaml: content' }, + { fileName: 'malformed3.yml', type: 'YAML', content: 'null' }, + ], + }) + ) + + const [selectorItems, paramCount] = mockEnvironmentFileSelector.selectEnvironmentFile.getCall(0).args + + assert(mockEnvironmentFileSelector.selectEnvironmentFile.calledOnce) + assert.strictEqual(selectorItems.length, 2) + assert.strictEqual(paramCount, 2) + + // Check valid1.json + assert.strictEqual(selectorItems[0].fileName, 'valid1.json') + assert.strictEqual(selectorItems[0].hasMatchingTemplatePath, false) + assert.deepStrictEqual(selectorItems[0].compatibleParameters, [ + { ParameterKey: 'Param1', ParameterValue: 'value1' }, + ]) + + // Check valid2.yaml + assert.strictEqual(selectorItems[1].fileName, 'valid2.yaml') + assert.strictEqual(selectorItems[1].hasMatchingTemplatePath, false) + assert.deepStrictEqual(selectorItems[1].compatibleParameters, [ + { ParameterKey: 'Param2', ParameterValue: 'value2' }, + ]) + }) + + it('should return undefined when parameter file selector returns undefined', async () => { + mockWorkspaceState.get.returns('test-env') + readdirStub.resolves([['params.json', 1]]) + fsStub.resolves(JSON.stringify({ parameters: { Param1: 'value1' } })) + + parseEnvironmentFilesStub.resolves([ + { + fileName: 'params.json', + deploymentConfig: { + parameters: { Param1: 'value1' }, + }, + }, + ]) + + mockEnvironmentFileSelector.selectEnvironmentFile.resolves(undefined) + + const result = await environmentManager.selectEnvironmentFile('template.yaml', [{ name: 'Param1' }]) + + assert.strictEqual(result, undefined) + }) + }) + + describe('refreshSelectedEnvironment', () => { + it('should unselect environment if environment not in config', async () => { + mockWorkspaceState.get.returns('not-found') + const mockEnvironmentLookup = { env1: { name: 'env1', profile: 'profile1' } } + fsStub.resolves(JSON.stringify({ environments: mockEnvironmentLookup })) + + await environmentManager.refreshSelectedEnvironment() + + assert(mockWorkspaceState.update.calledWith('aws.cloudformation.selectedEnvironment', undefined)) + }) + + it('should keep environment if environment is valid', async () => { + mockWorkspaceState.get.returns('env1') + const mockEnvironmentLookup = { env1: { name: 'env1', profile: 'profile1' } } + fsStub.resolves(JSON.stringify({ environments: mockEnvironmentLookup })) + + await environmentManager.refreshSelectedEnvironment() + + assert(mockWorkspaceState.update.notCalled) + }) + + it('should keep environment if environment is undefined', async () => { + mockWorkspaceState.get.returns(undefined) + const mockEnvironmentLookup = { env1: { name: 'env1', profile: 'profile1' } } + fsStub.resolves(JSON.stringify({ environments: mockEnvironmentLookup })) + + await environmentManager.refreshSelectedEnvironment() + + assert(mockWorkspaceState.update.notCalled) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/commands/cfnCommands.test.ts b/packages/core/src/test/awsService/cloudformation/commands/cfnCommands.test.ts new file mode 100644 index 00000000000..493a187578b --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/commands/cfnCommands.test.ts @@ -0,0 +1,421 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { OnStackFailure, Parameter } from '@aws-sdk/client-cloudformation' +import { + rerunValidateAndDeployCommand, + extractToParameterPositionCursorCommand, + promptForOptionalFlags, + promptToSaveToFile, + addResourceTypesCommand, + removeResourceTypeCommand, +} from '../../../../awsService/cloudformation/commands/cfnCommands' +import { OptionalFlagMode } from '../../../../awsService/cloudformation/stacks/actions/stackActionRequestType' +import * as inputBox from '../../../../awsService/cloudformation/ui/inputBox' +import { fs } from '../../../../shared/fs/fs' +import { ResourceTypeNode } from '../../../../awsService/cloudformation/explorer/nodes/resourceTypeNode' + +describe('CfnCommands', function () { + let sandbox: sinon.SinonSandbox + let registerCommandStub: sinon.SinonStub + + beforeEach(function () { + sandbox = sinon.createSandbox() + registerCommandStub = sandbox.stub(vscode.commands, 'registerCommand').returns({ + dispose: () => {}, + } as vscode.Disposable) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('rerunValidateAndDeployCommand', function () { + it('should register rerun last validation command', function () { + const result = rerunValidateAndDeployCommand() + assert.ok(result) + assert.ok(registerCommandStub.calledOnce) + assert.strictEqual(registerCommandStub.firstCall.args[0], 'aws.cloudformation.api.rerunValidateAndDeploy') + }) + }) + + describe('extractToParameterPositionCursorCommand', function () { + it('should register extract to parameter command', function () { + const mockClient = {} as any + const result = extractToParameterPositionCursorCommand(mockClient) + assert.ok(result) + assert.ok(registerCommandStub.calledOnce) + assert.strictEqual( + registerCommandStub.firstCall.args[0], + 'aws.cloudformation.extractToParameter.positionCursor' + ) + }) + }) + + describe('promptForOptionalFlags', function () { + let chooseOptionalFlagModeStub: sinon.SinonStub + let getOnStackFailureStub: sinon.SinonStub + let getIncludeNestedStacksStub: sinon.SinonStub + let getTagsStub: sinon.SinonStub + let getImportExistingResourcesStub: sinon.SinonStub + let getDeploymentModeStub: sinon.SinonStub + + beforeEach(function () { + chooseOptionalFlagModeStub = sandbox.stub(inputBox, 'chooseOptionalFlagSuggestion') + getOnStackFailureStub = sandbox.stub(inputBox, 'getOnStackFailure') + getIncludeNestedStacksStub = sandbox.stub(inputBox, 'getIncludeNestedStacks') + getTagsStub = sandbox.stub(inputBox, 'getTags') + getImportExistingResourcesStub = sandbox.stub(inputBox, 'getImportExistingResources') + getDeploymentModeStub = sandbox.stub(inputBox, 'getDeploymentMode') + }) + + it('should return skip mode with existing file flags', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.Skip) + + const fileFlags = { + onStackFailure: OnStackFailure.DO_NOTHING, + includeNestedStacks: true, + tags: [{ Key: 'test', Value: 'value' }], + importExistingResources: false, + } + + const result = await promptForOptionalFlags(fileFlags) + + assert.deepStrictEqual(result, { + onStackFailure: OnStackFailure.DO_NOTHING, + includeNestedStacks: true, + tags: [{ Key: 'test', Value: 'value' }], + importExistingResources: false, + shouldSaveOptions: false, + }) + }) + + it('should use dev friendly defaults', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.DevFriendly) + getTagsStub.resolves(undefined) + + const result = await promptForOptionalFlags() + + assert.deepStrictEqual(result, { + onStackFailure: OnStackFailure.DO_NOTHING, + includeNestedStacks: true, + tags: undefined, + importExistingResources: true, + deploymentMode: undefined, + }) + }) + + it('should set shouldSaveOptions to true when input mode collects new values', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.Input) + getDeploymentModeStub.resolves(undefined) + getOnStackFailureStub.resolves(OnStackFailure.DELETE) + getIncludeNestedStacksStub.resolves(true) + getTagsStub.resolves([{ Key: 'Environment', Value: 'prod' }]) + getImportExistingResourcesStub.resolves(false) + + const result = await promptForOptionalFlags() + + assert.deepStrictEqual(result, { + onStackFailure: OnStackFailure.DELETE, + includeNestedStacks: true, + tags: [{ Key: 'Environment', Value: 'prod' }], + importExistingResources: false, + deploymentMode: undefined, + shouldSaveOptions: true, + }) + }) + + it('should not prompt for deployment mode for CREATE stack', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.Input) + getOnStackFailureStub.resolves(OnStackFailure.ROLLBACK) + getIncludeNestedStacksStub.resolves(false) + getTagsStub.resolves(undefined) + getImportExistingResourcesStub.resolves(false) + + const result = await promptForOptionalFlags() + + assert.ok(getDeploymentModeStub.notCalled) + assert.ok(getOnStackFailureStub.calledOnce) + assert.ok(getIncludeNestedStacksStub.calledOnce) + assert.ok(getImportExistingResourcesStub.calledOnce) + assert.deepStrictEqual(result, { + onStackFailure: OnStackFailure.ROLLBACK, + includeNestedStacks: false, + tags: undefined, + importExistingResources: false, + deploymentMode: undefined, + shouldSaveOptions: true, + }) + }) + + it('should not prompt for deployment mode when stack is REVIEW_IN_PROGRESS', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.Input) + getOnStackFailureStub.resolves(OnStackFailure.ROLLBACK) + getIncludeNestedStacksStub.resolves(false) + getTagsStub.resolves(undefined) + getImportExistingResourcesStub.resolves(false) + + const stackDetails = { StackName: 'test-stack', StackStatus: 'REVIEW_IN_PROGRESS' as any } + const result = await promptForOptionalFlags(undefined, stackDetails as any) + + assert.ok(getDeploymentModeStub.notCalled) + assert.strictEqual(result?.deploymentMode, undefined) + }) + + it('should prompt for deployment mode and other flags when not REVERT_DRIFT', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.Input) + getDeploymentModeStub.resolves(undefined) + getOnStackFailureStub.resolves(OnStackFailure.DELETE) + getIncludeNestedStacksStub.resolves(true) + getTagsStub.resolves(undefined) + getImportExistingResourcesStub.resolves(true) + + const stackDetails = { StackName: 'test-stack' } + await promptForOptionalFlags(undefined, stackDetails as any) + + assert.ok(getDeploymentModeStub.calledOnce) + assert.ok(getOnStackFailureStub.calledOnce) + assert.ok(getIncludeNestedStacksStub.calledOnce) + assert.ok(getImportExistingResourcesStub.calledOnce) + }) + + it('should skip other prompts when deploymentMode is REVERT_DRIFT', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.Input) + getDeploymentModeStub.resolves('REVERT_DRIFT') + getTagsStub.resolves(undefined) + + const stackDetails = { StackName: 'test-stack' } + const result = await promptForOptionalFlags(undefined, stackDetails as any) + + assert.ok(getDeploymentModeStub.calledOnce) + assert.ok(getOnStackFailureStub.notCalled) + assert.ok(getIncludeNestedStacksStub.notCalled) + assert.ok(getImportExistingResourcesStub.notCalled) + assert.deepStrictEqual(result, { + onStackFailure: undefined, + includeNestedStacks: undefined, + tags: undefined, + importExistingResources: undefined, + deploymentMode: 'REVERT_DRIFT', + shouldSaveOptions: true, + }) + }) + + it('should include deploymentMode from fileFlags in skip mode', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.Skip) + + const fileFlags = { + onStackFailure: OnStackFailure.DO_NOTHING, + includeNestedStacks: true, + tags: undefined, + importExistingResources: false, + deploymentMode: 'COMPLETE_REPLACEMENT' as any, + } + + const result = await promptForOptionalFlags(fileFlags) + + assert.deepStrictEqual(result, { + onStackFailure: OnStackFailure.DO_NOTHING, + includeNestedStacks: true, + tags: undefined, + importExistingResources: false, + deploymentMode: 'COMPLETE_REPLACEMENT', + shouldSaveOptions: false, + }) + }) + + it('should default to REVERT_DRIFT in skip mode when conditions are met', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.Skip) + + const fileFlags = { + onStackFailure: OnStackFailure.ROLLBACK, + includeNestedStacks: false, + tags: undefined, + importExistingResources: false, + } + + const stackDetails = { StackName: 'test-stack' } + const result = await promptForOptionalFlags(fileFlags, stackDetails as any) + + assert.deepStrictEqual(result, { + onStackFailure: OnStackFailure.ROLLBACK, + includeNestedStacks: false, + tags: undefined, + importExistingResources: false, + deploymentMode: 'REVERT_DRIFT', + shouldSaveOptions: false, + }) + }) + + it('should not default to REVERT_DRIFT in skip mode when stack does not exist', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.Skip) + + const fileFlags = { + onStackFailure: OnStackFailure.ROLLBACK, + includeNestedStacks: false, + tags: undefined, + importExistingResources: false, + } + + const result = await promptForOptionalFlags(fileFlags) + + assert.deepStrictEqual(result, { + onStackFailure: OnStackFailure.ROLLBACK, + includeNestedStacks: false, + tags: undefined, + importExistingResources: false, + deploymentMode: undefined, + shouldSaveOptions: false, + }) + }) + + it('should not default to REVERT_DRIFT in skip mode when stack is REVIEW_IN_PROGRESS', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.Skip) + + const fileFlags = { + onStackFailure: OnStackFailure.ROLLBACK, + includeNestedStacks: false, + tags: undefined, + importExistingResources: false, + } + + const stackDetails = { StackName: 'test-stack', StackStatus: 'REVIEW_IN_PROGRESS' as any } + const result = await promptForOptionalFlags(fileFlags, stackDetails as any) + + assert.deepStrictEqual(result, { + onStackFailure: OnStackFailure.ROLLBACK, + includeNestedStacks: false, + tags: undefined, + importExistingResources: false, + deploymentMode: undefined, + shouldSaveOptions: false, + }) + }) + + it('should not default to REVERT_DRIFT in skip mode when includeNestedStacks is true', async function () { + chooseOptionalFlagModeStub.resolves(OptionalFlagMode.Skip) + + const fileFlags = { + onStackFailure: OnStackFailure.ROLLBACK, + includeNestedStacks: true, + tags: undefined, + importExistingResources: false, + } + + const stackDetails = { StackName: 'test-stack' } + const result = await promptForOptionalFlags(fileFlags, stackDetails as any) + + assert.deepStrictEqual(result, { + onStackFailure: OnStackFailure.ROLLBACK, + includeNestedStacks: true, + tags: undefined, + importExistingResources: false, + deploymentMode: undefined, + shouldSaveOptions: false, + }) + }) + }) + + describe('promptToSaveToFile', function () { + let shouldSaveFlagsToFileStub: sinon.SinonStub + let getFilePathStub: sinon.SinonStub + let workspaceConfigStub: sinon.SinonStub + let workspaceAsRelativePathStub: sinon.SinonStub + let fsWriteFileStub: sinon.SinonStub + + beforeEach(function () { + shouldSaveFlagsToFileStub = sandbox.stub(inputBox, 'shouldSaveFlagsToFile') + getFilePathStub = sandbox.stub(inputBox, 'getFilePath') + workspaceConfigStub = sandbox.stub(vscode.workspace, 'getConfiguration') + workspaceAsRelativePathStub = sandbox.stub(vscode.workspace, 'asRelativePath') + fsWriteFileStub = sandbox.stub(fs, 'writeFile') + }) + + it('should return early when user chooses not to save', async function () { + shouldSaveFlagsToFileStub.resolves(false) + + await promptToSaveToFile('/test/env', undefined, undefined) + + assert(getFilePathStub.notCalled) + assert(fsWriteFileStub.notCalled) + }) + + it('should save JSON file with correct format', async function () { + shouldSaveFlagsToFileStub.resolves(true) + getFilePathStub.resolves('/test/env/config.json') + workspaceAsRelativePathStub.returns('config.json') + + const mockConfig = { + get: sandbox.stub(), + } + mockConfig.get.withArgs('tabSize', 2).returns(2) + mockConfig.get.withArgs('insertSpaces', true).returns(true) + workspaceConfigStub.returns(mockConfig) + + const parameters: Parameter[] = [ + { ParameterKey: 'Environment', ParameterValue: 'test' }, + { ParameterKey: 'InstanceType', ParameterValue: 't3.micro' }, + ] + + const optionalFlags = { + onStackFailure: OnStackFailure.ROLLBACK, + includeNestedStacks: false, + tags: [{ Key: 'Project', Value: 'MyApp' }], + importExistingResources: true, + } + + await promptToSaveToFile('/test/env', optionalFlags, parameters) + + assert(fsWriteFileStub.calledOnce) + const [filePath, content] = fsWriteFileStub.getCall(0).args + assert.strictEqual(filePath, '/test/env/config.json') + + const parsed = JSON.parse(content) + assert.deepStrictEqual(parsed['parameters'], { + Environment: 'test', + InstanceType: 't3.micro', + }) + assert.deepStrictEqual(parsed['tags'], { Project: 'MyApp' }) + assert.strictEqual(parsed['on-stack-failure'], OnStackFailure.ROLLBACK) + assert.strictEqual(parsed['include-nested-stacks'], false) + assert.strictEqual(parsed['import-existing-resources'], true) + }) + }) + + describe('addResourceTypesCommand', function () { + it('should register add resource types command', function () { + const mockResourcesManager = { selectResourceTypes: sinon.stub() } as any + const result = addResourceTypesCommand(mockResourcesManager) + assert.ok(result) + assert.ok(registerCommandStub.calledOnce) + assert.strictEqual(registerCommandStub.firstCall.args[0], 'aws.cloudformation.api.addResourceTypes') + }) + }) + + describe('removeResourceTypeCommand', function () { + it('should register remove resource type command', function () { + const mockResourcesManager = { removeResourceType: sinon.stub() } as any + const result = removeResourceTypeCommand(mockResourcesManager) + assert.ok(result) + assert.ok(registerCommandStub.calledOnce) + assert.strictEqual(registerCommandStub.firstCall.args[0], 'aws.cloudformation.removeResourceType') + }) + + it('should call removeResourceType with node typeName', async function () { + const mockResourcesManager = { removeResourceType: sinon.stub().resolves() } as any + removeResourceTypeCommand(mockResourcesManager) + + const commandHandler = registerCommandStub.firstCall.args[1] + const mockNode = { typeName: 'AWS::S3::Bucket' } as ResourceTypeNode + + await commandHandler(mockNode) + + assert.ok(mockResourcesManager.removeResourceType.calledOnceWith('AWS::S3::Bucket')) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/commands/cursorPositioning.test.ts b/packages/core/src/test/awsService/cloudformation/commands/cursorPositioning.test.ts new file mode 100644 index 00000000000..4ad80eb216f --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/commands/cursorPositioning.test.ts @@ -0,0 +1,26 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' + +describe('CursorPositioning', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('cursor positioning', function () { + it('should position cursor correctly', function () { + // Basic test structure - implementation depends on actual CursorPositioning module + assert.ok(true, 'CursorPositioning test placeholder') + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/explorer/nodes/resourceNode.test.ts b/packages/core/src/test/awsService/cloudformation/explorer/nodes/resourceNode.test.ts new file mode 100644 index 00000000000..03c2a765d7d --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/explorer/nodes/resourceNode.test.ts @@ -0,0 +1,33 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { TreeItemCollapsibleState } from 'vscode' +import { ResourceNode } from '../../../../../awsService/cloudformation/explorer/nodes/resourceNode' + +describe('ResourceNode', function () { + let resourceNode: ResourceNode + + beforeEach(function () { + resourceNode = new ResourceNode('my-bucket-123', 'AWS::S3::Bucket') + }) + + describe('constructor', function () { + it('should set correct properties', function () { + assert.strictEqual(resourceNode.label, 'my-bucket-123') + assert.strictEqual(resourceNode.resourceIdentifier, 'my-bucket-123') + assert.strictEqual(resourceNode.resourceType, 'AWS::S3::Bucket') + assert.strictEqual(resourceNode.contextValue, 'resource') + assert.strictEqual(resourceNode.collapsibleState, TreeItemCollapsibleState.None) + }) + }) + + describe('getChildren', function () { + it('should return empty array', async function () { + const children = await resourceNode.getChildren() + assert.strictEqual(children.length, 0) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/explorer/nodes/resourceTypeNode.test.ts b/packages/core/src/test/awsService/cloudformation/explorer/nodes/resourceTypeNode.test.ts new file mode 100644 index 00000000000..afb7866c265 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/explorer/nodes/resourceTypeNode.test.ts @@ -0,0 +1,111 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { TreeItemCollapsibleState } from 'vscode' +import { ResourceTypeNode } from '../../../../../awsService/cloudformation/explorer/nodes/resourceTypeNode' +import { ResourceList } from '../../../../../awsService/cloudformation/resources/resourceRequestTypes' +import { ResourcesManager } from '../../../../../awsService/cloudformation/resources/resourcesManager' + +describe('ResourceTypeNode', function () { + let mockResourceList: ResourceList + let resourceTypeNode: ResourceTypeNode + let mockResourcesManager: ResourcesManager + + beforeEach(function () { + mockResourceList = { + typeName: 'AWS::S3::Bucket', + resourceIdentifiers: ['bucket-1', 'bucket-2', 'bucket-3'], + } + + mockResourcesManager = {} as ResourcesManager + + resourceTypeNode = new ResourceTypeNode('AWS::S3::Bucket', mockResourcesManager, mockResourceList) + }) + + describe('constructor', function () { + it('should set correct properties when resourceList is provided', function () { + assert.strictEqual(resourceTypeNode.label, 'AWS::S3::Bucket') + assert.strictEqual(resourceTypeNode.description, '(3)') + assert.strictEqual(resourceTypeNode.contextValue, 'resourceType') + assert.strictEqual(resourceTypeNode.collapsibleState, TreeItemCollapsibleState.Collapsed) + }) + + it('should set correct properties when resourceList is undefined', function () { + const lazyNode = new ResourceTypeNode('AWS::Lambda::Function', mockResourcesManager) + assert.strictEqual(lazyNode.label, 'AWS::Lambda::Function') + assert.strictEqual(lazyNode.description, undefined) + assert.strictEqual(lazyNode.contextValue, 'resourceType') + }) + }) + + describe('getChildren', function () { + it('should return resource nodes for each identifier', async function () { + const children = await resourceTypeNode.getChildren() + assert.strictEqual(children.length, 3) + + const labels = children.map((child) => child.label) + assert(labels.includes('bucket-1')) + assert(labels.includes('bucket-2')) + assert(labels.includes('bucket-3')) + }) + + it('should lazy load resources when not provided', async function () { + const lazyResourceList: ResourceList = { + typeName: 'AWS::DynamoDB::Table', + resourceIdentifiers: ['table-1'], + } + + mockResourcesManager.loadResourceType = async () => {} + mockResourcesManager.get = () => [lazyResourceList] + + const lazyNode = new ResourceTypeNode('AWS::DynamoDB::Table', mockResourcesManager) + const children = await lazyNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].label, 'table-1') + }) + }) + + describe('empty resource list', function () { + it('should handle empty resource identifiers', async function () { + const emptyResourceList: ResourceList = { + typeName: 'AWS::Lambda::Function', + resourceIdentifiers: [], + } + + const emptyNode = new ResourceTypeNode('AWS::Lambda::Function', mockResourcesManager, emptyResourceList) + assert.strictEqual(emptyNode.description, '(0)') + + const children = await emptyNode.getChildren() + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].label, 'No resources found') + }) + }) + + describe('pagination', function () { + it('should show load more node when nextToken exists', async function () { + const paginatedList: ResourceList = { + typeName: 'AWS::EC2::Instance', + resourceIdentifiers: ['i-1', 'i-2'], + nextToken: 'token123', + } + + const paginatedNode = new ResourceTypeNode('AWS::EC2::Instance', mockResourcesManager, paginatedList) + assert.strictEqual(paginatedNode.description, '(2+)') + assert.strictEqual(paginatedNode.contextValue, 'resourceTypeWithMore') + + const children = await paginatedNode.getChildren() + assert.strictEqual(children.length, 3) + assert.strictEqual(children[2].label, '[Load More...]') + }) + + it('should not show load more node when no nextToken', async function () { + const children = await resourceTypeNode.getChildren() + assert.strictEqual(children.length, 3) + assert(!children.some((child) => child.label === '[Load More...]')) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/explorer/nodes/resourcesNode.test.ts b/packages/core/src/test/awsService/cloudformation/explorer/nodes/resourcesNode.test.ts new file mode 100644 index 00000000000..47d5807835f --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/explorer/nodes/resourcesNode.test.ts @@ -0,0 +1,63 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { TreeItemCollapsibleState } from 'vscode' +import { ResourcesNode } from '../../../../../awsService/cloudformation/explorer/nodes/resourcesNode' +import { ResourcesManager } from '../../../../../awsService/cloudformation/resources/resourcesManager' +import { ResourceList } from '../../../../../awsService/cloudformation/resources/resourceRequestTypes' + +describe('ResourcesNode', function () { + let resourcesNode: ResourcesNode + let mockResourcesManager: ResourcesManager + + beforeEach(function () { + mockResourcesManager = {} as ResourcesManager + resourcesNode = new ResourcesNode(mockResourcesManager) + }) + + describe('constructor', function () { + it('should set correct properties', function () { + assert.strictEqual(resourcesNode.label, 'Resources') + assert.strictEqual(resourcesNode.contextValue, 'resourceSection') + assert.strictEqual(resourcesNode.collapsibleState, TreeItemCollapsibleState.Collapsed) + }) + }) + + describe('getChildren', function () { + it('should return ResourceTypeNode for each selected type', async function () { + mockResourcesManager.getSelectedResourceTypes = () => ['AWS::S3::Bucket', 'AWS::Lambda::Function'] + mockResourcesManager.get = () => [] + + const children = await resourcesNode.getChildren() + assert.strictEqual(children.length, 2) + assert.strictEqual(children[0].label, 'AWS::S3::Bucket') + assert.strictEqual(children[1].label, 'AWS::Lambda::Function') + }) + + it('should pass loaded resourceList when available', async function () { + const loadedResource: ResourceList = { + typeName: 'AWS::S3::Bucket', + resourceIdentifiers: ['bucket-1', 'bucket-2'], + } + + mockResourcesManager.getSelectedResourceTypes = () => ['AWS::S3::Bucket'] + mockResourcesManager.get = () => [loadedResource] + + const children = await resourcesNode.getChildren() + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].label, 'AWS::S3::Bucket') + assert.strictEqual(children[0].description, '(2)') + }) + + it('should return empty array when no types selected', async function () { + mockResourcesManager.getSelectedResourceTypes = () => [] + mockResourcesManager.get = () => [] + + const children = await resourcesNode.getChildren() + assert.strictEqual(children.length, 0) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/explorer/nodes/stacksNode.test.ts b/packages/core/src/test/awsService/cloudformation/explorer/nodes/stacksNode.test.ts new file mode 100644 index 00000000000..d37d847432d --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/explorer/nodes/stacksNode.test.ts @@ -0,0 +1,204 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { TreeItemCollapsibleState } from 'vscode' +import { StacksNode } from '../../../../../awsService/cloudformation/explorer/nodes/stacksNode' +import { StacksManager } from '../../../../../awsService/cloudformation/stacks/stacksManager' +import { ChangeSetsManager } from '../../../../../awsService/cloudformation/stacks/changeSetsManager' +import { StackSummary } from '@aws-sdk/client-cloudformation' + +describe('StacksNode', function () { + let stacksNode: StacksNode + let mockStacksManager: sinon.SinonStubbedInstance + let mockChangeSetsManager: ChangeSetsManager + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + mockStacksManager = { + get: sandbox.stub(), + hasMore: sandbox.stub(), + isLoaded: sandbox.stub(), + ensureLoaded: sandbox.stub(), + loadMoreStacks: sandbox.stub(), + } as any + mockChangeSetsManager = {} as ChangeSetsManager + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('constructor', function () { + it('should set correct properties when not loaded', function () { + mockStacksManager.get.returns([]) + mockStacksManager.hasMore.returns(false) + mockStacksManager.isLoaded.returns(false) + + stacksNode = new StacksNode(mockStacksManager as any, mockChangeSetsManager) + + assert.strictEqual(stacksNode.label, 'Stacks') + assert.strictEqual(stacksNode.collapsibleState, TreeItemCollapsibleState.Collapsed) + assert.strictEqual(stacksNode.description, undefined) + assert.strictEqual(stacksNode.contextValue, 'stackSection') + }) + + it('should set description when loaded', function () { + const mockStacks: StackSummary[] = [ + { StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary, + { StackName: 'stack-2', StackStatus: 'UPDATE_COMPLETE' } as StackSummary, + ] + mockStacksManager.get.returns(mockStacks) + mockStacksManager.hasMore.returns(false) + mockStacksManager.isLoaded.returns(true) + + stacksNode = new StacksNode(mockStacksManager as any, mockChangeSetsManager) + + assert.strictEqual(stacksNode.description, '(2)') + assert.strictEqual(stacksNode.contextValue, 'stackSection') + }) + + it('should set contextValue to stackSectionWithMore when hasMore', function () { + const mockStacks: StackSummary[] = [ + { StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary, + ] + mockStacksManager.get.returns(mockStacks) + mockStacksManager.hasMore.returns(true) + mockStacksManager.isLoaded.returns(true) + + stacksNode = new StacksNode(mockStacksManager as any, mockChangeSetsManager) + + assert.strictEqual(stacksNode.description, '(1+)') + assert.strictEqual(stacksNode.contextValue, 'stackSectionWithMore') + }) + }) + + describe('getChildren', function () { + beforeEach(function () { + mockStacksManager.get.returns([]) + mockStacksManager.hasMore.returns(false) + mockStacksManager.isLoaded.returns(false) + stacksNode = new StacksNode(mockStacksManager as any, mockChangeSetsManager) + }) + + it('should call ensureLoaded', async function () { + mockStacksManager.ensureLoaded.resolves() + + await stacksNode.getChildren() + + assert.strictEqual(mockStacksManager.ensureLoaded.calledOnce, true) + }) + + it('should return StackNode for each stack', async function () { + const mockStacks: StackSummary[] = [ + { StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary, + { StackName: 'stack-2', StackStatus: 'UPDATE_COMPLETE' } as StackSummary, + ] + mockStacksManager.ensureLoaded.resolves() + mockStacksManager.get.returns(mockStacks) + mockStacksManager.hasMore.returns(false) + mockStacksManager.isLoaded.returns(true) + + const children = await stacksNode.getChildren() + + assert.strictEqual(children.length, 2) + assert.strictEqual(children[0].label, 'stack-1') + assert.strictEqual(children[1].label, 'stack-2') + }) + + it('should include LoadMoreStacksNode when hasMore', async function () { + const mockStacks: StackSummary[] = [ + { StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary, + ] + mockStacksManager.ensureLoaded.resolves() + mockStacksManager.get.returns(mockStacks) + mockStacksManager.hasMore.returns(true) + mockStacksManager.isLoaded.returns(true) + + const children = await stacksNode.getChildren() + + assert.strictEqual(children.length, 2) + assert.strictEqual(children[0].label, 'stack-1') + assert.strictEqual(children[1].label, '[Load More...]') + assert.strictEqual(children[1].contextValue, 'loadMoreStacks') + }) + + it('should update node description after load', async function () { + const mockStacks: StackSummary[] = [ + { StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary, + ] + mockStacksManager.ensureLoaded.resolves() + mockStacksManager.get.returns(mockStacks) + mockStacksManager.hasMore.returns(false) + mockStacksManager.isLoaded.returns(true) + + await stacksNode.getChildren() + + assert.strictEqual(stacksNode.description, '(1)') + assert.strictEqual(stacksNode.contextValue, 'stackSection') + }) + + it('should return empty array when no stacks', async function () { + mockStacksManager.ensureLoaded.resolves() + mockStacksManager.get.returns([]) + mockStacksManager.hasMore.returns(false) + mockStacksManager.isLoaded.returns(true) + + const children = await stacksNode.getChildren() + + assert.strictEqual(children.length, 0) + }) + }) + + describe('loadMoreStacks', function () { + beforeEach(function () { + mockStacksManager.get.returns([]) + mockStacksManager.hasMore.returns(false) + mockStacksManager.isLoaded.returns(false) + stacksNode = new StacksNode(mockStacksManager as any, mockChangeSetsManager) + }) + + it('should call stacksManager.loadMoreStacks', async function () { + mockStacksManager.loadMoreStacks.resolves() + + await stacksNode.loadMoreStacks() + + assert.strictEqual(mockStacksManager.loadMoreStacks.calledOnce, true) + }) + + it('should update node description after loading more', async function () { + const mockStacks: StackSummary[] = [ + { StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary, + { StackName: 'stack-2', StackStatus: 'UPDATE_COMPLETE' } as StackSummary, + ] + mockStacksManager.loadMoreStacks.resolves() + mockStacksManager.get.returns(mockStacks) + mockStacksManager.hasMore.returns(true) + mockStacksManager.isLoaded.returns(true) + + await stacksNode.loadMoreStacks() + + assert.strictEqual(stacksNode.description, '(2+)') + assert.strictEqual(stacksNode.contextValue, 'stackSectionWithMore') + }) + + it('should update contextValue when no more stacks', async function () { + const mockStacks: StackSummary[] = [ + { StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary, + ] + mockStacksManager.loadMoreStacks.resolves() + mockStacksManager.get.returns(mockStacks) + mockStacksManager.hasMore.returns(false) + mockStacksManager.isLoaded.returns(true) + + await stacksNode.loadMoreStacks() + + assert.strictEqual(stacksNode.description, '(1)') + assert.strictEqual(stacksNode.contextValue, 'stackSection') + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/explorer/regionManager.test.ts b/packages/core/src/test/awsService/cloudformation/explorer/regionManager.test.ts new file mode 100644 index 00000000000..73428afddb9 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/explorer/regionManager.test.ts @@ -0,0 +1,46 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { CloudFormationRegionManager } from '../../../../awsService/cloudformation/explorer/regionManager' +import { RegionProvider } from '../../../../shared/regions/regionProvider' + +describe('CloudFormationRegionManager', function () { + let sandbox: sinon.SinonSandbox + let mockRegionProvider: RegionProvider + let regionManager: CloudFormationRegionManager + + beforeEach(function () { + sandbox = sinon.createSandbox() + mockRegionProvider = { + getRegions: () => [ + { id: 'us-east-1', name: 'US East (N. Virginia)' }, + { id: 'us-west-2', name: 'US West (Oregon)' }, + ], + } as any + regionManager = new CloudFormationRegionManager(mockRegionProvider) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('getSelectedRegion', function () { + it('should return a region string', function () { + const region = regionManager.getSelectedRegion() + assert(typeof region === 'string') + }) + }) + + describe('updateSelectedRegion', function () { + it('should accept a region string', async function () { + const testRegion = 'us-east-1' + await regionManager.updateSelectedRegion(testRegion) + // Test passes if no error thrown + assert(true) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/grammar.test.ts b/packages/core/src/test/awsService/cloudformation/grammar.test.ts new file mode 100644 index 00000000000..54906112866 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/grammar.test.ts @@ -0,0 +1,76 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { fs } from '../../../shared/fs/fs' +import * as path from 'path' + +describe('CloudFormation Grammar', function () { + let grammar: any + + before(async function () { + // Load grammar from toolkit syntaxes directory + const grammarPath = path.join(__dirname, '../../../../../../toolkit/syntaxes/cloudformation.tmLanguage.json') + const content = await fs.readFileText(grammarPath) + grammar = JSON.parse(content) + }) + + describe('Grammar Structure', function () { + it('should have correct basic structure', function () { + assert.strictEqual(grammar.name, 'CloudFormation') + assert.strictEqual(grammar.scopeName, 'source.cloudformation') + assert.ok(grammar.fileTypes.includes('template')) + assert.ok(grammar.fileTypes.includes('cfn')) + }) + + it('should include dual-format detection patterns', function () { + assert.strictEqual(grammar.patterns.length, 2) + + // JSON detection pattern + assert.strictEqual(grammar.patterns[0].begin, '^\\s*\\{') + assert.strictEqual(grammar.patterns[0].name, 'meta.cloudformation.json') + assert.strictEqual(grammar.patterns[0].patterns[0].include, 'source.json') + + // YAML detection pattern + assert.strictEqual(grammar.patterns[1].begin, '^(?!\\s*\\{)') + assert.strictEqual(grammar.patterns[1].name, 'meta.cloudformation.yaml') + }) + + it('should have repository with required patterns', function () { + const requiredPatterns = ['cfn-top-level-keys', 'cfn-logical-ids', 'cfn-functions'] + + for (const pattern of requiredPatterns) { + assert.ok(grammar.repository[pattern], `Pattern ${pattern} should be defined`) + } + }) + }) + + describe('CloudFormation-Specific Patterns', function () { + it('should match top-level CloudFormation sections', function () { + const pattern = grammar.repository['cfn-top-level-keys'].patterns[0] + assert.ok(pattern) + }) + + it('should have logical ID patterns for all major sections', function () { + const logicalIds = grammar.repository['cfn-logical-ids'] + assert.ok(logicalIds) + assert.ok(logicalIds.patterns) + + // Check that we have patterns for Resources, Parameters, Conditions, Outputs, and Mappings + const sectionNames: (string | undefined)[] = logicalIds.patterns.map((pattern: any) => { + const match = (pattern.begin as string).match(/\^\(([^)]+)\)/) + return match ? match[1] : undefined + }) + + const expectedSections = ['Resources', 'Parameters', 'Conditions', 'Outputs', 'Mappings'] + for (const section of expectedSections) { + assert.ok( + sectionNames.some((name) => name && name.includes(section)), + `Should have pattern for ${section} section` + ) + } + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/lsp-server/utils.test.ts b/packages/core/src/test/awsService/cloudformation/lsp-server/utils.test.ts new file mode 100644 index 00000000000..7ebfeb282bd --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/lsp-server/utils.test.ts @@ -0,0 +1,311 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import { + addWindows, + dedupeAndGetLatestVersions, + extractPlatformAndArch, + useOldLinuxVersion, + mapLegacyLinux, + CfnTarget, + CfnLspVersion, +} from '../../../../awsService/cloudformation/lsp-server/utils' +import { CLibCheck } from '../../../../awsService/cloudformation/lsp-server/CLibCheck' +import { LspVersion } from '../../../../shared/lsp/types' + +describe('addWindows', () => { + it('adds windows target when win32 exists and windows does not', () => { + const targets: CfnTarget[] = [ + { platform: 'darwin', arch: 'arm64', contents: [] }, + { platform: 'linux', arch: 'x64', contents: [] }, + { platform: 'win32', arch: 'x64', contents: [] }, + ] + + const result = addWindows(targets) + + assert.strictEqual(result.length, 4) + assert.ok(result.some((t) => t.platform === 'windows' && t.arch === 'x64')) + }) + + it('does not add windows target when windows already exists', () => { + const targets: CfnTarget[] = [ + { platform: 'darwin', arch: 'arm64', contents: [] }, + { platform: 'win32', arch: 'x64', contents: [] }, + { platform: 'windows', arch: 'x64', contents: [] }, + ] + + const result = addWindows(targets) + + assert.strictEqual(result.length, 3) + }) + + it('does not add windows target when no win32 exists', () => { + const targets: CfnTarget[] = [ + { platform: 'darwin', arch: 'arm64', contents: [] }, + { platform: 'linux', arch: 'x64', contents: [] }, + ] + + const result = addWindows(targets) + + assert.strictEqual(result.length, 2) + }) + + it('adds windows for multiple win32 architectures', () => { + const targets: CfnTarget[] = [ + { platform: 'win32', arch: 'x64', contents: [] }, + { platform: 'win32', arch: 'arm64', contents: [] }, + ] + + const result = addWindows(targets) + + assert.strictEqual(result.length, 4) + assert.strictEqual(result.filter((t) => t.platform === 'windows' && t.arch === 'x64').length, 1) + assert.strictEqual(result.filter((t) => t.platform === 'windows' && t.arch === 'arm64').length, 1) + }) +}) + +describe('extractPlatformAndArch', () => { + it('extracts platform, arch, and nodejs from standard filename', () => { + const result = extractPlatformAndArch('cloudformation-languageserver-1.2.0-beta-darwin-arm64-node22.zip') + + assert.strictEqual(result.platform, 'darwin') + assert.strictEqual(result.arch, 'arm64') + assert.strictEqual(result.nodejs, '22') + }) + + it('extracts linux platform with x64 arch', () => { + const result = extractPlatformAndArch('cloudformation-languageserver-1.2.0-beta-linux-x64-node22.zip') + + assert.strictEqual(result.platform, 'linux') + assert.strictEqual(result.arch, 'x64') + assert.strictEqual(result.nodejs, '22') + }) + + it('extracts linuxglib2.28 platform', () => { + const result = extractPlatformAndArch('cloudformation-languageserver-1.2.0-beta-linuxglib2.28-arm64-node18.zip') + + assert.strictEqual(result.platform, 'linuxglib2.28') + assert.strictEqual(result.arch, 'arm64') + assert.strictEqual(result.nodejs, '18') + }) + + it('extracts win32 platform', () => { + const result = extractPlatformAndArch('cloudformation-languageserver-1.2.0-beta-win32-x64-node22.zip') + + assert.strictEqual(result.platform, 'win32') + assert.strictEqual(result.arch, 'x64') + assert.strictEqual(result.nodejs, '22') + }) + + it('handles filename without node version', () => { + const result = extractPlatformAndArch('cloudformation-languageserver-1.1.0-darwin-arm64.zip') + + assert.strictEqual(result.platform, 'darwin') + assert.strictEqual(result.arch, 'arm64') + assert.strictEqual(result.nodejs, undefined) + }) + + it('handles alpha version with timestamp', () => { + const result = extractPlatformAndArch( + 'cloudformation-languageserver-1.2.0-202512020323-alpha-darwin-arm64-node22.zip' + ) + + assert.strictEqual(result.platform, 'darwin') + assert.strictEqual(result.arch, 'arm64') + assert.strictEqual(result.nodejs, '22') + }) + + it('throws error for invalid filename', () => { + assert.throws(() => extractPlatformAndArch('invalid-file.zip'), /Could not extract platform/) + }) + + it('throws error for unsupported architecture', () => { + assert.throws( + () => extractPlatformAndArch('cloudformation-languageserver-1.0.0-darwin-arm32-node22.zip'), + /Could not extract platform/ + ) + }) +}) + +describe('useOldLinuxVersion', () => { + let sandbox: sinon.SinonSandbox + let originalPlatform: PropertyDescriptor | undefined + + beforeEach(() => { + sandbox = sinon.createSandbox() + originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') + }) + + afterEach(() => { + sandbox.restore() + if (originalPlatform) { + Object.defineProperty(process, 'platform', originalPlatform) + } + delete process.env.SNAP + }) + + it('returns false on non-linux platforms', () => { + Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true }) + + assert.strictEqual(useOldLinuxVersion(), false) + }) + + it('returns true in SNAP environment on linux', () => { + Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }) + process.env.SNAP = '/snap/something' + + assert.strictEqual(useOldLinuxVersion(), true) + }) + + it('returns false when GLIBCXX version cannot be determined', () => { + Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }) + sandbox.stub(CLibCheck, 'getGLibCXXVersions').returns({ maxFound: undefined, allAvailable: [] }) + + assert.strictEqual(useOldLinuxVersion(), false) + }) + + it('returns true when GLIBCXX version is older than 3.4.29', () => { + Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }) + sandbox.stub(CLibCheck, 'getGLibCXXVersions').returns({ maxFound: '3.4.28', allAvailable: ['3.4.28'] }) + + assert.strictEqual(useOldLinuxVersion(), true) + }) + + it('returns false when GLIBCXX version is 3.4.29', () => { + Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }) + sandbox.stub(CLibCheck, 'getGLibCXXVersions').returns({ maxFound: '3.4.29', allAvailable: ['3.4.29'] }) + + assert.strictEqual(useOldLinuxVersion(), false) + }) + + it('returns false when GLIBCXX version is newer than 3.4.29', () => { + Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }) + sandbox.stub(CLibCheck, 'getGLibCXXVersions').returns({ maxFound: '3.4.32', allAvailable: ['3.4.32'] }) + + assert.strictEqual(useOldLinuxVersion(), false) + }) +}) + +describe('dedupeAndGetLatestVersions', () => { + for (const prefix of ['v', '']) { + it(`handles versions with timestamp: ${prefix}`, () => { + const result = dedupeAndGetLatestVersions( + generateLspVersion(['0.0.1-2020', '0.0.2-2024', '0.0.3-2026', '0.0.2-2025', '0.0.3-2030'], prefix) + ) + + assert.strictEqual(result.length, 3) + assert.strictEqual(result[0].serverVersion, '0.0.3-2030') + assert.strictEqual(result[1].serverVersion, '0.0.2-2025') + assert.strictEqual(result[2].serverVersion, '0.0.1-2020') + }) + + it('handles versions with timestamp and environment', () => { + const result = dedupeAndGetLatestVersions( + generateLspVersion( + ['0.0.1-2020-alpha', '0.0.2-2024-beta', '0.0.3-2026-alpha', '0.0.2-2025-prod', '0.0.3-2030-beta'], + prefix + ) + ) + + assert.strictEqual(result.length, 3) + assert.strictEqual(result[0].serverVersion, '0.0.3-2030-beta') + assert.strictEqual(result[1].serverVersion, '0.0.2-2025-prod') + assert.strictEqual(result[2].serverVersion, '0.0.1-2020-alpha') + }) + } + + function generateLspVersion(versions: string[], prefix: string = ''): LspVersion[] { + return versions.map((version) => { + return { serverVersion: `${prefix}${version}`, targets: [], isDelisted: false } + }) + } +}) + +describe('mapLegacyLinux', () => { + const darwinContent = { filename: 'darwin.zip', url: 'https://example.com/darwin.zip', hashes: ['abc'], bytes: 100 } + const linuxContent = { filename: 'linux.zip', url: 'https://example.com/linux.zip', hashes: ['def'], bytes: 200 } + const legacyContent = { filename: 'legacy.zip', url: 'https://example.com/legacy.zip', hashes: ['ghi'], bytes: 300 } + const winContent = { filename: 'win.zip', url: 'https://example.com/win.zip', hashes: ['jkl'], bytes: 400 } + + it('remaps linuxglib2.28 to linux and removes original linux target', () => { + const versions: CfnLspVersion[] = [ + { + serverVersion: '1.0.0', + isDelisted: false, + targets: [ + { platform: 'darwin', arch: 'arm64', contents: [darwinContent] }, + { platform: 'linux', arch: 'x64', contents: [linuxContent] }, + { platform: 'linuxglib2.28', arch: 'x64', contents: [legacyContent], nodejs: '18' }, + { platform: 'win32', arch: 'x64', contents: [winContent] }, + ], + }, + ] + + const result = mapLegacyLinux(versions) + + assert.strictEqual(result.length, 1) + assert.strictEqual(result[0].serverVersion, '1.0.0') + assert.strictEqual(result[0].isDelisted, false) + assert.strictEqual(result[0].targets.length, 3) + assert.deepStrictEqual(result[0].targets[0], { platform: 'darwin', arch: 'arm64', contents: [darwinContent] }) + assert.deepStrictEqual(result[0].targets[1], { + platform: 'linux', + arch: 'x64', + contents: [legacyContent], + nodejs: '18', + }) + assert.deepStrictEqual(result[0].targets[2], { platform: 'win32', arch: 'x64', contents: [winContent] }) + }) + + it('returns version unchanged when no linuxglib2.28 target exists', () => { + const versions: CfnLspVersion[] = [ + { + serverVersion: '2.0.0', + isDelisted: true, + targets: [ + { platform: 'darwin', arch: 'arm64', contents: [darwinContent] }, + { platform: 'linux', arch: 'x64', contents: [linuxContent] }, + ], + }, + ] + + const result = mapLegacyLinux(versions) + + assert.strictEqual(result.length, 1) + assert.deepStrictEqual(result[0], versions[0]) + }) + + it('handles multiple versions with mixed legacy targets', () => { + const versions: CfnLspVersion[] = [ + { + serverVersion: '1.0.0', + isDelisted: false, + targets: [ + { platform: 'darwin', arch: 'arm64', contents: [] }, + { platform: 'linuxglib2.28', arch: 'x64', contents: [legacyContent] }, + ], + }, + { + serverVersion: '2.0.0', + isDelisted: false, + targets: [{ platform: 'darwin', arch: 'arm64', contents: [] }], + }, + ] + + const result = mapLegacyLinux(versions) + + assert.strictEqual(result.length, 2) + assert.strictEqual(result[0].serverVersion, '1.0.0') + assert.strictEqual(result[0].targets.length, 2) + assert.deepStrictEqual(result[0].targets[1], { platform: 'linux', arch: 'x64', contents: [legacyContent] }) + assert.deepStrictEqual(result[1], versions[1]) + }) + + it('handles empty versions array', () => { + assert.deepStrictEqual(mapLegacyLinux([]), []) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/relatedResources/relatedResourcesApi.test.ts b/packages/core/src/test/awsService/cloudformation/relatedResources/relatedResourcesApi.test.ts new file mode 100644 index 00000000000..31b4b3b926e --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/relatedResources/relatedResourcesApi.test.ts @@ -0,0 +1,137 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { + getAuthoredResourceTypes, + getRelatedResourceTypes, + insertRelatedResources, +} from '../../../../awsService/cloudformation/relatedResources/relatedResourcesApi' +import { + GetAuthoredResourceTypesRequest, + GetRelatedResourceTypesRequest, + InsertRelatedResourcesRequest, +} from '../../../../awsService/cloudformation/relatedResources/relatedResourcesProtocol' + +describe('RelatedResourcesApi', function () { + let sandbox: sinon.SinonSandbox + let mockClient: any + + beforeEach(function () { + sandbox = sinon.createSandbox() + mockClient = { + sendRequest: sandbox.stub(), + } + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('getAuthoredResourceTypes', function () { + it('should send request with template URI and return resource types', async function () { + const templateUri = 'file:///test/template.yaml' + const expectedTypes = ['AWS::S3::Bucket', 'AWS::Lambda::Function'] + + mockClient.sendRequest.resolves(expectedTypes) + + const result = await getAuthoredResourceTypes(mockClient, templateUri) + + assert.deepStrictEqual(result, expectedTypes) + assert.ok(mockClient.sendRequest.calledOnce) + assert.ok(mockClient.sendRequest.calledWith(GetAuthoredResourceTypesRequest, templateUri)) + }) + + it('should return empty array when no resources found', async function () { + const templateUri = 'file:///test/empty.yaml' + + mockClient.sendRequest.resolves([]) + + const result = await getAuthoredResourceTypes(mockClient, templateUri) + + assert.deepStrictEqual(result, []) + }) + }) + + describe('getRelatedResourceTypes', function () { + it('should send request with resource type and return related types', async function () { + const params = { parentResourceType: 'AWS::S3::Bucket' } + const expectedTypes = ['AWS::Lambda::Function', 'AWS::IAM::Role'] + + mockClient.sendRequest.resolves(expectedTypes) + + const result = await getRelatedResourceTypes(mockClient, params) + + assert.deepStrictEqual(result, expectedTypes) + assert.ok(mockClient.sendRequest.calledOnce) + assert.ok(mockClient.sendRequest.calledWith(GetRelatedResourceTypesRequest, params)) + }) + + it('should return empty array when no related types found', async function () { + const params = { parentResourceType: 'AWS::Custom::Resource' } + + mockClient.sendRequest.resolves([]) + + const result = await getRelatedResourceTypes(mockClient, params) + + assert.deepStrictEqual(result, []) + }) + }) + + describe('insertRelatedResources', function () { + it('should send request and return code action', async function () { + const params = { + templateUri: 'file:///test/template.yaml', + relatedResourceTypes: ['AWS::Lambda::Function'], + parentResourceType: 'AWS::S3::Bucket', + } + const expectedAction = { + title: 'Insert 1 related resources', + kind: 'refactor', + edit: { + changes: { + 'file:///test/template.yaml': [], + }, + }, + data: { + scrollToPosition: { line: 5, character: 0 }, + firstLogicalId: 'LambdaFunctionRelatedToS3Bucket', + }, + } + + mockClient.sendRequest.resolves(expectedAction) + + const result = await insertRelatedResources(mockClient, params) + + assert.deepStrictEqual(result, expectedAction) + assert.ok(mockClient.sendRequest.calledOnce) + assert.ok(mockClient.sendRequest.calledWith(InsertRelatedResourcesRequest, params)) + }) + + it('should handle multiple resource types', async function () { + const params = { + templateUri: 'file:///test/template.yaml', + relatedResourceTypes: ['AWS::Lambda::Function', 'AWS::IAM::Role'], + parentResourceType: 'AWS::S3::Bucket', + } + const expectedAction = { + title: 'Insert 2 related resources', + kind: 'refactor', + edit: { + changes: { + 'file:///test/template.yaml': [], + }, + }, + } + + mockClient.sendRequest.resolves(expectedAction) + + const result = await insertRelatedResources(mockClient, params) + + assert.strictEqual(result.title, 'Insert 2 related resources') + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/resources/resourcesManager.test.ts b/packages/core/src/test/awsService/cloudformation/resources/resourcesManager.test.ts new file mode 100644 index 00000000000..b1ac434c5f0 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/resources/resourcesManager.test.ts @@ -0,0 +1,279 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { ResourcesManager } from '../../../../awsService/cloudformation/resources/resourcesManager' +import { ResourceSelector } from '../../../../awsService/cloudformation/ui/resourceSelector' +import { ResourceStateResult } from '../../../../awsService/cloudformation/resources/resourceRequestTypes' +import { Range, SnippetString, TextEditor, window } from 'vscode' +import { getLogger } from '../../../../shared/logger' +import globals from '../../../../shared/extensionGlobals' + +describe('ResourcesManager - applyCompletionSnippet', () => { + let sandbox: sinon.SinonSandbox + let mockClient: any + let mockResourceSelector: ResourceSelector + let resourcesManager: ResourcesManager + let mockEditor: Partial + let windowStub: sinon.SinonStub + const baseResourceStateResult = { + successfulImports: new Map(), + failedImports: new Map(), + } + + const createResult = (overrides?: Partial): ResourceStateResult => ({ + ...baseResourceStateResult, + ...overrides, + }) + + beforeEach(() => { + sandbox = sinon.createSandbox() + mockClient = { + sendRequest: sandbox.stub(), + } + mockResourceSelector = {} as ResourceSelector + + mockEditor = { + insertSnippet: sandbox.stub().resolves(true), + edit: sandbox.stub().resolves(true), + document: { + lineCount: 100, + lineAt: sandbox.stub().returns({ range: { end: { line: 99, character: 0 } } }), + } as any, + } + + windowStub = sandbox.stub(window, 'activeTextEditor').get(() => mockEditor) + + sandbox.stub(getLogger(), 'warn') + sandbox.stub(getLogger(), 'info') + sandbox.stub(getLogger(), 'error') + + resourcesManager = new ResourcesManager(mockClient, mockResourceSelector) + }) + + afterEach(() => { + sandbox.restore() + }) + + it('should insert snippet at server-provided position', async () => { + const result = createResult({ + completionItem: { + label: 'Import Resource', + textEdit: { + range: { + start: { line: 5, character: 10 }, + end: { line: 5, character: 10 }, + }, + newText: ' "MyBucket": {\n "Type": "AWS::S3::Bucket"\n }', + }, + }, + }) + + await (resourcesManager as any).applyCompletionSnippet(result) + + assert.ok((mockEditor.insertSnippet as sinon.SinonStub).calledOnce) + const [snippetArg, rangeArg] = (mockEditor.insertSnippet as sinon.SinonStub).firstCall.args + + assert.ok(snippetArg instanceof SnippetString) + assert.strictEqual(snippetArg.value, result.completionItem!.textEdit!.newText) + + assert.ok(rangeArg instanceof Range) + assert.strictEqual(rangeArg.start.line, 5) + assert.strictEqual(rangeArg.start.character, 10) + assert.strictEqual(rangeArg.end.line, 5) + assert.strictEqual(rangeArg.end.character, 10) + }) + + it('should handle snippet with tabstops', async () => { + const result = createResult({ + completionItem: { + label: 'Clone Resource', + textEdit: { + range: { + start: { line: 10, character: 0 }, + end: { line: 10, character: 0 }, + }, + newText: '"BucketName": "${1:enter new identifier for MyBucket}"', + }, + }, + }) + + await (resourcesManager as any).applyCompletionSnippet(result) + + assert.ok((mockEditor.insertSnippet as sinon.SinonStub).calledOnce) + const [snippetArg] = (mockEditor.insertSnippet as sinon.SinonStub).firstCall.args + assert.strictEqual(snippetArg.value, result.completionItem!.textEdit!.newText) + }) + + it('should not insert when completionItem is missing', async () => { + const result = createResult() + + await (resourcesManager as any).applyCompletionSnippet(result) + + assert.ok((mockEditor.insertSnippet as sinon.SinonStub).notCalled) + }) + + it('should not insert when textEdit is missing', async () => { + const result = createResult({ + completionItem: { + label: 'Test', + }, + }) + + await (resourcesManager as any).applyCompletionSnippet(result) + + assert.ok((mockEditor.insertSnippet as sinon.SinonStub).notCalled) + }) + + it('should not insert when no active editor', async () => { + windowStub.get(() => undefined) + + const result = createResult({ + completionItem: { + label: 'Test', + textEdit: { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + newText: 'test', + }, + }, + }) + + await (resourcesManager as any).applyCompletionSnippet(result) + + assert.ok((mockEditor.insertSnippet as sinon.SinonStub).notCalled) + }) + + it('should handle different range positions', async () => { + const result = createResult({ + completionItem: { + label: 'Test', + textEdit: { + range: { + start: { line: 100, character: 50 }, + end: { line: 105, character: 20 }, + }, + newText: 'replacement text', + }, + }, + }) + + await (resourcesManager as any).applyCompletionSnippet(result) + + const [, rangeArg] = (mockEditor.insertSnippet as sinon.SinonStub).firstCall.args + assert.strictEqual(rangeArg.start.line, 100) + assert.strictEqual(rangeArg.start.character, 50) + assert.strictEqual(rangeArg.end.line, 105) + assert.strictEqual(rangeArg.end.character, 20) + }) + + it('should handle multi-line snippet text', async () => { + const multiLineText = `"MyResource": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "\${1:enter new identifier}" + } +}` + + const result = createResult({ + completionItem: { + label: 'Test', + textEdit: { + range: { + start: { line: 20, character: 4 }, + end: { line: 20, character: 4 }, + }, + newText: multiLineText, + }, + }, + }) + + await (resourcesManager as any).applyCompletionSnippet(result) + + assert.ok((mockEditor.insertSnippet as sinon.SinonStub).calledOnce) + const [snippetArg] = (mockEditor.insertSnippet as sinon.SinonStub).firstCall.args + assert.strictEqual(snippetArg.value, multiLineText) + }) + + it('should add newlines when target line does not exist', async () => { + const mockDocument = { + lineCount: 10, + lineAt: sinon.stub().returns({ range: { end: { line: 9, character: 20 } } }), + } + ;(mockEditor as any).document = mockDocument + ;(mockEditor as any).edit = sinon.stub().resolves(true) + + const result = createResult({ + completionItem: { + label: 'Test', + textEdit: { + range: { + start: { line: 15, character: 0 }, + end: { line: 15, character: 0 }, + }, + newText: 'test content', + }, + }, + }) + + await (resourcesManager as any).applyCompletionSnippet(result) + + // Should call edit to add newlines + assert.ok(((mockEditor as any).edit as sinon.SinonStub).calledOnce) + + // Should still call insertSnippet + assert.ok((mockEditor.insertSnippet as sinon.SinonStub).calledOnce) + }) +}) + +describe('ResourcesManager - removeResourceType', () => { + let sandbox: sinon.SinonSandbox + let mockClient: any + let mockResourceSelector: ResourceSelector + let resourcesManager: ResourcesManager + let globalStateStub: sinon.SinonStub + + beforeEach(() => { + sandbox = sinon.createSandbox() + mockClient = { sendRequest: sandbox.stub() } + mockResourceSelector = {} as ResourceSelector + globalStateStub = sandbox.stub(globals.globalState, 'update').resolves() + sandbox.stub(globals.globalState, 'tryGet').returns(['AWS::S3::Bucket', 'AWS::Lambda::Function']) + resourcesManager = new ResourcesManager(mockClient, mockResourceSelector) + }) + + afterEach(() => { + sandbox.restore() + }) + + it('should remove resource type from selected types', async () => { + await resourcesManager.removeResourceType('AWS::S3::Bucket') + + assert.ok(globalStateStub.calledOnce) + const [key, updatedTypes] = globalStateStub.firstCall.args + assert.strictEqual(key, 'aws.cloudformation.selectedResourceTypes') + assert.deepStrictEqual(updatedTypes, ['AWS::Lambda::Function']) + }) + + it('should notify listeners after removing resource type', async () => { + const listener = sandbox.stub() + resourcesManager.addListener(listener) + + await resourcesManager.removeResourceType('AWS::Lambda::Function') + + assert.ok(listener.calledOnce) + }) + + it('should handle removing non-existent resource type', async () => { + await resourcesManager.removeResourceType('AWS::DynamoDB::Table') + + assert.ok(globalStateStub.calledOnce) + const [, updatedTypes] = globalStateStub.firstCall.args + assert.deepStrictEqual(updatedTypes, ['AWS::S3::Bucket', 'AWS::Lambda::Function']) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/resources/sample-template.yaml b/packages/core/src/test/awsService/cloudformation/resources/sample-template.yaml new file mode 100644 index 00000000000..e354cc7a3a9 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/resources/sample-template.yaml @@ -0,0 +1,54 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'Sample CloudFormation template for testing' + +Parameters: + InstanceType: + Type: String + Default: t2.micro + Description: EC2 instance type + +Resources: + MyInstance: + Type: AWS::EC2::Instance + Properties: + ImageId: ami-12345678 + InstanceType: !Ref InstanceType + SecurityGroups: + - !Ref MySecurityGroup + UserData: + Fn::Base64: !Sub | + #!/bin/bash + echo "Hello World" + + MySecurityGroup: + Type: 'AWS::EC2::SecurityGroup' + Properties: + GroupDescription: Security group for testing + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: 0.0.0.0/0 + + MyBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Sub '${AWS::StackName}-test-bucket' + VersioningConfiguration: + Status: Enabled + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + +Outputs: + InstanceId: + Description: Instance ID + Value: !Ref MyInstance + Export: + Name: !Sub '${AWS::StackName}-InstanceId' + + BucketName: + Description: S3 Bucket Name + Value: !Ref MyBucket diff --git a/packages/core/src/test/awsService/cloudformation/resources/template-with-json.yaml b/packages/core/src/test/awsService/cloudformation/resources/template-with-json.yaml new file mode 100644 index 00000000000..92975737ad0 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/resources/template-with-json.yaml @@ -0,0 +1,77 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'CloudFormation template with embedded JSON' + +Resources: + MyRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + } + Policies: + - PolicyName: MyPolicy + PolicyDocument: > + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "*" + } + ] + } + + MyBucket: + Type: AWS::S3::Bucket + Properties: + NotificationConfiguration: + CloudWatchConfigurations: + - Event: s3:ObjectCreated:* + CloudWatchConfiguration: + LogGroupName: !Ref MyLogGroup + FilterPattern: | + { + "eventSource": ["aws:s3"], + "eventName": { + "prefix": "ObjectCreated" + } + } + + MyFunction: + Type: AWS::Lambda::Function + Properties: + Code: + ZipFile: | + import json + def lambda_handler(event, context): + response = { + "statusCode": 200, + "body": json.dumps({ + "message": "Hello from Lambda!", + "event": event + }) + } + return response + Environment: + Variables: + CONFIG: '{"debug": true, "timeout": 30}' + + MyLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: /aws/lambda/my-function diff --git a/packages/core/src/test/awsService/cloudformation/stacks/actions/changeSetDeletionWorkflow.test.ts b/packages/core/src/test/awsService/cloudformation/stacks/actions/changeSetDeletionWorkflow.test.ts new file mode 100644 index 00000000000..9dec3c1e3d3 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/stacks/actions/changeSetDeletionWorkflow.test.ts @@ -0,0 +1,84 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { SinonSandbox, SinonStub, createSandbox } from 'sinon' +import { commands } from 'vscode' +import { ChangeSetDeletion } from '../../../../../awsService/cloudformation/stacks/actions/changeSetDeletionWorkflow' +import { + StackActionPhase, + StackActionState, +} from '../../../../../awsService/cloudformation/stacks/actions/stackActionRequestType' +import { commandKey } from '../../../../../awsService/cloudformation/utils' +import { globals } from '../../../../../shared' + +describe('ChangeSetDeletion', function () { + let sandbox: SinonSandbox + + beforeEach(function () { + sandbox = createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('delete', function () { + let mockClient: any + let executeCommandStub: SinonStub + let getChangeSetDeletionStatusStub: SinonStub + let describeChangeSetDeletionStatusStub: SinonStub + + beforeEach(function () { + mockClient = { sendRequest: sandbox.stub().resolves({}) } + executeCommandStub = sandbox.stub(commands, 'executeCommand').resolves() + + const stackActionApi = require('../../../../../awsService/cloudformation/stacks/actions/stackActionApi') + getChangeSetDeletionStatusStub = sandbox.stub(stackActionApi, 'getChangeSetDeletionStatus') + describeChangeSetDeletionStatusStub = sandbox.stub(stackActionApi, 'describeChangeSetDeletionStatus') + sandbox.stub(stackActionApi, 'deleteChangeSet').resolves() + + sandbox.stub(globals.clock, 'setInterval').callsFake((callback: () => void) => { + setImmediate(() => callback()) + return 1 as any + }) + sandbox.stub(globals.clock, 'clearInterval') + }) + + it('should call refresh command after successful deletion', async function () { + getChangeSetDeletionStatusStub.resolves({ + phase: StackActionPhase.DELETION_COMPLETE, + state: StackActionState.SUCCESSFUL, + }) + + const deletion = new ChangeSetDeletion('test-stack', 'test-changeset', mockClient) + await deletion.delete() + await new Promise((resolve) => setImmediate(resolve)) + + assert.ok(executeCommandStub.calledWith(commandKey('stacks.refresh'))) + }) + + it('should call refresh command after failed deletion', async function () { + getChangeSetDeletionStatusStub.resolves({ phase: StackActionPhase.DELETION_FAILED }) + describeChangeSetDeletionStatusStub.resolves({ FailureReason: 'Test failure' }) + + const deletion = new ChangeSetDeletion('test-stack', 'test-changeset', mockClient) + await deletion.delete() + await new Promise((resolve) => setImmediate(resolve)) + + assert.ok(executeCommandStub.calledWith(commandKey('stacks.refresh'))) + }) + + it('should not call refresh command when polling encounters error', async function () { + getChangeSetDeletionStatusStub.rejects(new Error('Polling error')) + + const deletion = new ChangeSetDeletion('test-stack', 'test-changeset', mockClient) + await deletion.delete() + await new Promise((resolve) => setImmediate(resolve)) + + assert.ok(executeCommandStub.calledWith(commandKey('stacks.refresh'))) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/stacks/actions/deployment.test.ts b/packages/core/src/test/awsService/cloudformation/stacks/actions/deployment.test.ts new file mode 100644 index 00000000000..22b82d14b87 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/stacks/actions/deployment.test.ts @@ -0,0 +1,81 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { SinonSandbox, SinonStub, createSandbox } from 'sinon' +import { commands } from 'vscode' +import { Deployment } from '../../../../../awsService/cloudformation/stacks/actions/deploymentWorkflow' +import { + StackActionPhase, + StackActionState, +} from '../../../../../awsService/cloudformation/stacks/actions/stackActionRequestType' +import { commandKey } from '../../../../../awsService/cloudformation/utils' + +describe('Deployment', function () { + let sandbox: SinonSandbox + + beforeEach(function () { + sandbox = createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('pollForProgress', function () { + let mockClient: any + let mockCoordinator: any + let executeCommandStub: SinonStub + let getDeploymentStatusStub: SinonStub + let describeDeploymentStatusStub: SinonStub + let clock: any + + beforeEach(function () { + mockClient = { sendRequest: sandbox.stub().resolves({}) } + mockCoordinator = { setStack: sandbox.stub().resolves() } + executeCommandStub = sandbox.stub(commands, 'executeCommand').resolves() + + const stackActionApi = require('../../../../../awsService/cloudformation/stacks/actions/stackActionApi') + getDeploymentStatusStub = sandbox.stub(stackActionApi, 'getDeploymentStatus') + describeDeploymentStatusStub = sandbox.stub(stackActionApi, 'describeDeploymentStatus') + sandbox.stub(stackActionApi, 'deploy').resolves() + clock = sandbox.useFakeTimers() + }) + + it('should call refresh command after successful deployment', async function () { + getDeploymentStatusStub.resolves({ + phase: StackActionPhase.DEPLOYMENT_COMPLETE, + state: StackActionState.SUCCESSFUL, + }) + + const deployment = new Deployment('test-stack', 'test-changeset', mockClient, mockCoordinator) + await deployment.deploy() + await clock.tickAsync(1000) + + assert.ok(executeCommandStub.calledWith(commandKey('stacks.refresh'))) + }) + + it('should call refresh command after failed deployment', async function () { + getDeploymentStatusStub.resolves({ phase: StackActionPhase.DEPLOYMENT_FAILED }) + describeDeploymentStatusStub.resolves({ FailureReason: 'Test failure' }) + + const deployment = new Deployment('test-stack', 'test-changeset', mockClient, mockCoordinator) + await deployment.deploy() + await clock.tickAsync(1000) + + assert.ok(executeCommandStub.calledWith(commandKey('stacks.refresh'))) + }) + + it('should call refresh command when polling encounters error', async function () { + getDeploymentStatusStub.rejects(new Error('Polling error')) + + const deployment = new Deployment('test-stack', 'test-changeset', mockClient, mockCoordinator) + await deployment.deploy() + await clock.tickAsync(1000) + + assert.ok(executeCommandStub.calledWith(commandKey('stacks.refresh'))) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/stacks/actions/stackActionInputValidation.test.ts b/packages/core/src/test/awsService/cloudformation/stacks/actions/stackActionInputValidation.test.ts new file mode 100644 index 00000000000..ed7f4a17787 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/stacks/actions/stackActionInputValidation.test.ts @@ -0,0 +1,149 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { validateParameterValue } from '../../../../../awsService/cloudformation/stacks/actions/stackActionInputValidation' +import { TemplateParameter } from '../../../../../awsService/cloudformation/stacks/actions/stackActionRequestType' + +describe('validateParameterValue', function () { + describe('String parameters', function () { + it('should pass valid string with AllowedValues', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'String', + AllowedValues: ['value1', 'value2'], + } + assert.strictEqual(validateParameterValue('value1', param), undefined) + }) + + it('should fail invalid string with AllowedValues', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'String', + AllowedValues: ['value1', 'value2'], + } + assert.strictEqual(validateParameterValue('invalid', param), 'Value must be one of: value1, value2') + }) + + it('should pass valid pattern', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'String', + AllowedPattern: '^[0-9]+$', + } + assert.strictEqual(validateParameterValue('123', param), undefined) + }) + + it('should fail invalid pattern', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'String', + AllowedPattern: '^[0-9]+$', + } + assert.strictEqual(validateParameterValue('abc', param), 'Value must match pattern: ^[0-9]+$') + }) + + it('should handle boolean string values', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'String', + AllowedValues: ['true', 'false'], + } + assert.strictEqual(validateParameterValue('true', param), undefined) + assert.strictEqual(validateParameterValue('false', param), undefined) + assert.strictEqual(validateParameterValue('invalid', param), 'Value must be one of: true, false') + }) + + it('should handle numeric string values', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'String', + AllowedValues: ['1', '2'], + } + assert.strictEqual(validateParameterValue('1', param), undefined) + assert.strictEqual(validateParameterValue('2', param), undefined) + assert.strictEqual(validateParameterValue('3', param), 'Value must be one of: 1, 2') + }) + + it('should handle empty string values', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'String', + } + + assert.strictEqual(validateParameterValue('', param), undefined) + }) + }) + + describe('Number parameters', function () { + it('should pass valid number', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'Number', + } + assert.strictEqual(validateParameterValue('42', param), undefined) + }) + + it('should fail invalid number', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'Number', + } + assert.strictEqual(validateParameterValue('abc', param), 'Value must be a number') + }) + + it('should validate MinValue', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'Number', + MinValue: 10, + } + assert.strictEqual(validateParameterValue('5', param), 'Value must be at least 10') + assert.strictEqual(validateParameterValue('15', param), undefined) + }) + }) + + describe('CommaDelimitedList parameters', function () { + it('should pass valid comma-delimited list with AllowedValues', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'CommaDelimitedList', + AllowedValues: ['80', '443', '8080'], + } + assert.strictEqual(validateParameterValue('80,443', param), undefined) + }) + + it('should fail invalid items in comma-delimited list', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'CommaDelimitedList', + AllowedValues: ['80', '443', '8080'], + } + assert.strictEqual( + validateParameterValue('80,9000', param), + 'Invalid values: 9000. Must be one of: 80, 443, 8080' + ) + }) + + it('should validate pattern for each item', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'CommaDelimitedList', + AllowedPattern: '^[0-9]+$', + } + assert.strictEqual(validateParameterValue('80,443', param), undefined) + assert.strictEqual(validateParameterValue('80,abc', param), 'Values must match pattern: ^[0-9]+$') + }) + + it('should handle whitespace in comma-delimited list', function () { + const param: TemplateParameter = { + name: 'TestParam', + Type: 'CommaDelimitedList', + AllowedValues: ['80', '443'], + } + assert.strictEqual(validateParameterValue('80, 443', param), undefined) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/stacks/actions/validation.test.ts b/packages/core/src/test/awsService/cloudformation/stacks/actions/validation.test.ts new file mode 100644 index 00000000000..e8d317d6587 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/stacks/actions/validation.test.ts @@ -0,0 +1,113 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { SinonSandbox, SinonStub, createSandbox } from 'sinon' +import { commands } from 'vscode' +import { + getLastValidation, + setLastValidation, + Validation, +} from '../../../../../awsService/cloudformation/stacks/actions/validationWorkflow' +import { commandKey } from '../../../../../awsService/cloudformation/utils' + +describe('Validation', function () { + let sandbox: SinonSandbox + + beforeEach(function () { + sandbox = createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('last validation tracking', function () { + it('should get and set last validation', function () { + assert.strictEqual(getLastValidation(), undefined) + + const validation: any = { uri: 'test.yaml', stackName: 'test-stack' } + setLastValidation(validation) + assert.strictEqual(getLastValidation(), validation) + + setLastValidation(undefined) + assert.strictEqual(getLastValidation(), undefined) + }) + }) + + describe('refresh command', function () { + let mockClient: any + let mockDiffProvider: any + let executeCommandStub: SinonStub + let validateStub: any + let getValidationStatusStub: any + let describeValidationStatusStub: any + let clock: any + + beforeEach(function () { + mockClient = { sendRequest: sandbox.stub().resolves({ changeSetName: 'test-changeset' }) } + mockDiffProvider = { updateData: sandbox.stub() } + executeCommandStub = sandbox.stub(commands, 'executeCommand').resolves() + + const stackActionApi = require('../../../../../awsService/cloudformation/stacks/actions/stackActionApi') + validateStub = sandbox.stub(stackActionApi, 'validate').resolves({ changeSetName: 'test-changeset' }) + getValidationStatusStub = sandbox.stub(stackActionApi, 'getValidationStatus') + describeValidationStatusStub = sandbox.stub(stackActionApi, 'describeValidationStatus') + clock = sandbox.useFakeTimers() + }) + + it('should call refresh after validation starts', async function () { + const validation = new Validation('file:///test.yaml', 'test-stack', mockClient, mockDiffProvider) + await validation.validate() + + assert.ok(executeCommandStub.calledWith(commandKey('stacks.refresh'))) + }) + + it('should not call refresh when validation API fails', async function () { + validateStub.rejects(new Error('Validation API error')) + + const validation = new Validation('file:///test.yaml', 'test-stack', mockClient, mockDiffProvider) + await validation.validate() + + assert.ok(!executeCommandStub.calledWith(commandKey('stacks.refresh'))) + }) + + it('should call refresh after successful validation', async function () { + getValidationStatusStub.resolves({ + phase: 'VALIDATION_COMPLETE', + state: 'SUCCESSFUL', + changes: [], + }) + describeValidationStatusStub.resolves({ ValidationDetails: [] }) + + const validation = new Validation('file:///test.yaml', 'test-stack', mockClient, mockDiffProvider) + await validation.validate() + await clock.tickAsync(1000) + + assert.ok(executeCommandStub.calledWith(commandKey('stacks.refresh'))) + }) + + it('should call refresh after failed validation', async function () { + getValidationStatusStub.resolves({ phase: 'VALIDATION_FAILED' }) + describeValidationStatusStub.resolves({ FailureReason: 'Test failure' }) + + const validation = new Validation('file:///test.yaml', 'test-stack', mockClient, mockDiffProvider) + await validation.validate() + await clock.tickAsync(1000) + + assert.ok(executeCommandStub.calledWith(commandKey('stacks.refresh'))) + }) + + it('should call refresh when polling encounters error', async function () { + getValidationStatusStub.rejects(new Error('Polling error')) + + const validation = new Validation('file:///test.yaml', 'test-stack', mockClient, mockDiffProvider) + await validation.validate() + await clock.tickAsync(1000) + + assert.ok(executeCommandStub.calledWith(commandKey('stacks.refresh'))) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/stacks/actions/validationEnhanced.test.ts b/packages/core/src/test/awsService/cloudformation/stacks/actions/validationEnhanced.test.ts new file mode 100644 index 00000000000..b059ffa7603 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/stacks/actions/validationEnhanced.test.ts @@ -0,0 +1,26 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' + +describe('ValidationEnhanced', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('enhanced validation', function () { + it('should perform enhanced validation correctly', function () { + // Basic test structure - implementation depends on actual ValidationEnhanced module + assert.ok(true, 'ValidationEnhanced test placeholder') + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/stacks/stacksManager.test.ts b/packages/core/src/test/awsService/cloudformation/stacks/stacksManager.test.ts new file mode 100644 index 00000000000..6140cdc4539 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/stacks/stacksManager.test.ts @@ -0,0 +1,153 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { StacksManager } from '../../../../awsService/cloudformation/stacks/stacksManager' + +describe('StacksManager', () => { + let sandbox: sinon.SinonSandbox + let manager: StacksManager + let mockClient: any + + beforeEach(() => { + sandbox = sinon.createSandbox() + mockClient = { + sendRequest: sandbox.stub().resolves({ + stacks: [ + { StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' }, + { StackName: 'stack-2', StackStatus: 'UPDATE_IN_PROGRESS' }, + ], + nextToken: undefined, + }), + } + manager = new StacksManager(mockClient) + }) + + afterEach(() => { + manager.dispose() + sandbox.restore() + }) + + describe('updateStackStatus', () => { + beforeEach(async () => { + await new Promise((resolve) => { + manager.addListener(() => resolve()) + manager.reload() + }) + }) + + it('should update status of existing stack', () => { + manager.updateStackStatus('stack-1', 'UPDATE_COMPLETE') + + const stacks = manager.get() + const updatedStack = stacks.find((s) => s.StackName === 'stack-1') + assert.strictEqual(updatedStack?.StackStatus, 'UPDATE_COMPLETE') + }) + + it('should not affect other stacks', () => { + manager.updateStackStatus('stack-1', 'UPDATE_COMPLETE') + + const stacks = manager.get() + const otherStack = stacks.find((s) => s.StackName === 'stack-2') + assert.strictEqual(otherStack?.StackStatus, 'UPDATE_IN_PROGRESS') + }) + + it('should notify listeners when status updated', () => { + let listenerCalled = false + manager.addListener(() => { + listenerCalled = true + }) + + manager.updateStackStatus('stack-1', 'UPDATE_COMPLETE') + + assert.strictEqual(listenerCalled, true) + }) + + it('should do nothing if stack not found', () => { + const stacksBefore = manager.get() + manager.updateStackStatus('non-existent-stack', 'CREATE_COMPLETE') + const stacksAfter = manager.get() + + assert.deepStrictEqual(stacksBefore, stacksAfter) + }) + }) + + describe('lazy loading', () => { + it('should not load stacks on construction', () => { + assert.strictEqual(mockClient.sendRequest.called, false) + }) + + it('should return empty array when not loaded', () => { + const stacks = manager.get() + assert.deepStrictEqual(stacks, []) + }) + + it('should report not loaded initially', () => { + assert.strictEqual(manager.isLoaded(), false) + }) + + it('should load stacks on ensureLoaded', async () => { + await manager.ensureLoaded() + assert.strictEqual(mockClient.sendRequest.calledOnce, true) + }) + + it('should not reload on subsequent ensureLoaded calls', async () => { + await manager.ensureLoaded() + await manager.ensureLoaded() + assert.strictEqual(mockClient.sendRequest.calledOnce, true) + }) + + it('should report loaded after ensureLoaded', async () => { + await manager.ensureLoaded() + assert.strictEqual(manager.isLoaded(), true) + }) + + it('should return stacks after ensureLoaded', async () => { + await manager.ensureLoaded() + const stacks = manager.get() + assert.strictEqual(stacks.length, 2) + assert.strictEqual(stacks[0].StackName, 'stack-1') + }) + }) + + describe('clear', () => { + beforeEach(async () => { + await manager.ensureLoaded() + }) + + it('should clear stacks', () => { + manager.clear() + const stacks = manager.get() + assert.deepStrictEqual(stacks, []) + }) + + it('should reset loaded state', () => { + manager.clear() + assert.strictEqual(manager.isLoaded(), false) + }) + + it('should clear nextToken', () => { + manager.clear() + assert.strictEqual(manager.hasMore(), false) + }) + + it('should notify listeners', () => { + let listenerCalled = false + manager.addListener(() => { + listenerCalled = true + }) + manager.clear() + assert.strictEqual(listenerCalled, true) + }) + + it('should allow reload after clear', async () => { + manager.clear() + mockClient.sendRequest.resetHistory() + await manager.ensureLoaded() + assert.strictEqual(mockClient.sendRequest.calledOnce, true) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/telemetryOptIn.test.ts b/packages/core/src/test/awsService/cloudformation/telemetryOptIn.test.ts new file mode 100644 index 00000000000..d28fde1dbf6 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/telemetryOptIn.test.ts @@ -0,0 +1,121 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { ExtensionContext } from 'vscode' +import { handleTelemetryOptIn } from '../../../awsService/cloudformation/telemetryOptIn' +import { CloudFormationTelemetrySettings } from '../../../awsService/cloudformation/extensionConfig' +import { commandKey } from '../../../awsService/cloudformation/utils' +import globals from '../../../shared/extensionGlobals' + +describe('telemetryOptIn', function () { + let mockContext: ExtensionContext + let mockSettings: CloudFormationTelemetrySettings + let globalState: Map + + beforeEach(function () { + globalState = new Map() + + mockContext = { + globalState: { + get: (key: string, defaultValue?: any) => globalState.get(key) ?? defaultValue, + update: async (key: string, value: any) => { + globalState.set(key, value) + }, + }, + } as any + + mockSettings = { + get: sinon.stub().returns(false), + update: sinon.stub().resolves(), + } as any + }) + + describe('promptTelemetryOptIn - automation mode', function () { + it('should return current setting without prompting in automation mode', async function () { + sinon.stub(require('../../../shared/vscode/env'), 'isAutomation').returns(true) + ;(mockSettings.get as sinon.SinonStub).returns(true) + + const result = await handleTelemetryOptIn(mockContext, mockSettings) + + assert.strictEqual(result, true) + }) + }) + + describe('promptTelemetryOptIn - user has responded', function () { + it('should return current setting if user has permanently responded', async function () { + globalState.set(commandKey('telemetry.hasResponded'), true) + ;(mockSettings.get as sinon.SinonStub).returns(true) + + const result = await handleTelemetryOptIn(mockContext, mockSettings) + + assert.strictEqual(result, true) + }) + }) + + describe('promptTelemetryOptIn - prompt timing', function () { + it('should not prompt if less than 30 days since last prompt', async function () { + const now = globals.clock.Date.now() + const twentyDaysAgo = now - 20 * 24 * 60 * 60 * 1000 + globalState.set(commandKey('telemetry.lastPromptDate'), twentyDaysAgo) + + const result = await handleTelemetryOptIn(mockContext, mockSettings) + + assert.strictEqual(result, false) + }) + }) + + describe('promptTelemetryOptIn - unpersisted response', function () { + it('should persist unpersisted Allow response', async function () { + globalState.set(commandKey('telemetry.unpersistedResponse'), 'Yes, Allow') + + const result = await handleTelemetryOptIn(mockContext, mockSettings) + + assert.strictEqual(result, true) + assert.ok((mockSettings.update as sinon.SinonStub).calledWith('enabled', true)) + assert.strictEqual(globalState.get(commandKey('telemetry.unpersistedResponse')), undefined) + }) + + it('should persist unpersisted Never response', async function () { + globalState.set(commandKey('telemetry.unpersistedResponse'), 'Never') + ;(mockSettings.update as sinon.SinonStub).resolves(true) + + const result = await handleTelemetryOptIn(mockContext, mockSettings) + + assert.strictEqual(result, false) + assert.ok((mockSettings.update as sinon.SinonStub).calledWith('enabled', false)) + assert.strictEqual(globalState.get(commandKey('telemetry.unpersistedResponse')), undefined) + }) + + it('should persist unpersisted Later response', async function () { + const lastPromptDate = globals.clock.Date.now() - 1000 + globalState.set(commandKey('telemetry.unpersistedResponse'), 'Not Now') + globalState.set(commandKey('telemetry.lastPromptDate'), lastPromptDate) + ;(mockSettings.update as sinon.SinonStub).resolves(true) + + const result = await handleTelemetryOptIn(mockContext, mockSettings) + + assert.strictEqual(result, false) + assert.ok((mockSettings.update as sinon.SinonStub).calledWith('enabled', false)) + assert.strictEqual(globalState.get(commandKey('telemetry.lastPromptDate')), lastPromptDate) + assert.strictEqual(globalState.get(commandKey('telemetry.unpersistedResponse')), undefined) + }) + + it('should clear all state if setting save fails', async function () { + globalState.set(commandKey('telemetry.unpersistedResponse'), 'Yes, Allow') + globalState.set(commandKey('telemetry.hasResponded'), true) + globalState.set(commandKey('telemetry.lastPromptDate'), globals.clock.Date.now()) + ;(mockSettings.update as sinon.SinonStub).resolves(false) + + const result = await handleTelemetryOptIn(mockContext, mockSettings) + + assert.strictEqual(result, true) + assert.strictEqual(globalState.get(commandKey('telemetry.unpersistedResponse')), undefined) + assert.strictEqual(globalState.get(commandKey('telemetry.hasResponded')), undefined) + assert.strictEqual(globalState.get(commandKey('telemetry.lastPromptDate')), undefined) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/ui/diffViewHelper.test.ts b/packages/core/src/test/awsService/cloudformation/ui/diffViewHelper.test.ts new file mode 100644 index 00000000000..0fa0da96df3 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/ui/diffViewHelper.test.ts @@ -0,0 +1,578 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import * as path from 'path' +import * as os from 'os' +import { DiffViewHelper } from '../../../../awsService/cloudformation/ui/diffViewHelper' +import { StackChange } from '../../../../awsService/cloudformation/stacks/actions/stackActionRequestType' +import { fs } from '../../../../shared/fs/fs' + +describe('DiffViewHelper', function () { + let sandbox: sinon.SinonSandbox + let writeFileStub: sinon.SinonStub + let executeCommandStub: sinon.SinonStub + let openTextDocumentStub: sinon.SinonStub + + beforeEach(function () { + sandbox = sinon.createSandbox() + writeFileStub = sandbox.stub(fs, 'writeFile') + executeCommandStub = sandbox.stub(vscode.commands, 'executeCommand') + openTextDocumentStub = sandbox.stub(vscode.workspace, 'openTextDocument') + }) + + afterEach(function () { + sandbox.restore() + }) + + async function testDiffGeneration(stackName: string, changes: StackChange[]) { + await DiffViewHelper.openDiff(stackName, changes) + + const tmpDir = os.tmpdir() + const beforePath = path.join(tmpDir, `${stackName}-before.json`) + const afterPath = path.join(tmpDir, `${stackName}-after.json`) + + return { beforePath, afterPath } + } + + function assertFileCallsAndParseData() { + assert.ok(writeFileStub.calledTwice) + const beforeCall = writeFileStub.getCall(0) + const afterCall = writeFileStub.getCall(1) + + const beforeData = JSON.parse(beforeCall.args[1]) + const afterData = JSON.parse(afterCall.args[1]) + + return { beforeData, afterData } + } + + describe('openDiff', function () { + it('should create diff files and open diff view for Add action', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Add', + logicalResourceId: 'TestResource', + afterContext: '{"Type": "AWS::S3::Bucket", "Properties": {"BucketName": "new-bucket"}}', + }, + }, + ] + + const { beforePath, afterPath } = await testDiffGeneration(stackName, changes) + + assert.ok(writeFileStub.calledTwice) + assert.ok(writeFileStub.calledWith(beforePath, '{}')) + assert.ok(writeFileStub.calledWith(afterPath, sinon.match.string)) + assert.ok( + executeCommandStub.calledWith( + 'vscode.diff', + sinon.match.any, + sinon.match.any, + `${stackName}: Before ↔ After` + ) + ) + }) + + it('should create diff files and open diff view for Remove action', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Remove', + logicalResourceId: 'TestResource', + beforeContext: '{"Type": "AWS::S3::Bucket", "Properties": {"BucketName": "old-bucket"}}', + }, + }, + ] + + const { beforePath, afterPath } = await testDiffGeneration(stackName, changes) + + assert.ok(writeFileStub.calledTwice) + assert.ok(writeFileStub.calledWith(beforePath, sinon.match.string)) + assert.ok(writeFileStub.calledWith(afterPath, '{}')) + assert.ok( + executeCommandStub.calledWith( + 'vscode.diff', + sinon.match.any, + sinon.match.any, + `${stackName}: Before ↔ After` + ) + ) + }) + + it('should create diff files for Modify action with beforeContext and afterContext', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Modify', + logicalResourceId: 'TestResource', + beforeContext: '{"Type": "AWS::S3::Bucket", "Properties": {"BucketName": "old-bucket"}}', + afterContext: '{"Type": "AWS::S3::Bucket", "Properties": {"BucketName": "new-bucket"}}', + }, + }, + ] + + await testDiffGeneration(stackName, changes) + + const { beforeData, afterData } = assertFileCallsAndParseData() + + assert.ok(beforeData.TestResource) + assert.ok(afterData.TestResource) + }) + + it('should handle Modify action with details when no context provided', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Modify', + logicalResourceId: 'ModifiedResource', + details: [ + { + Target: { + Name: 'BucketName', + BeforeValue: 'old-bucket', + AfterValue: 'new-bucket', + }, + }, + ], + }, + }, + ] + + await testDiffGeneration(stackName, changes) + + const { beforeData, afterData } = assertFileCallsAndParseData() + + assert.strictEqual(beforeData.ModifiedResource.BucketName, 'old-bucket') + assert.strictEqual(afterData.ModifiedResource.BucketName, 'new-bucket') + }) + + it('should handle invalid JSON in context gracefully', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Modify', + logicalResourceId: 'InvalidResource', + beforeContext: 'invalid-json', + afterContext: 'also-invalid-json', + }, + }, + ] + + await DiffViewHelper.openDiff(stackName, changes) + + const { beforeData, afterData } = assertFileCallsAndParseData() + + assert.deepStrictEqual(beforeData.InvalidResource, {}) + assert.deepStrictEqual(afterData.InvalidResource, {}) + }) + + it('should skip changes without logicalResourceId', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Add', + // Missing logicalResourceId + }, + }, + ] + + await DiffViewHelper.openDiff(stackName, changes) + + assert.ok(writeFileStub.calledTwice) + assert.ok(writeFileStub.calledWith(sinon.match.any, '{}')) + }) + + it('should open diff with selection when resourceId is provided', async function () { + const stackName = 'test-stack' + const resourceId = 'TargetResource' + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Add', + logicalResourceId: resourceId, + afterContext: '{"Type": "AWS::S3::Bucket"}', + }, + }, + ] + + const mockDocument = { + getText: () => `{\n "${resourceId}": {\n "Type": "AWS::S3::Bucket"\n }\n}`, + } + openTextDocumentStub.resolves(mockDocument) + + await DiffViewHelper.openDiff(stackName, changes, resourceId) + + assert.ok(executeCommandStub.calledTwice) + const secondCall = executeCommandStub.getCall(1) + assert.ok(secondCall.args[4]?.selection) + }) + + it('should handle resourceId not found in document', async function () { + const stackName = 'test-stack' + const resourceId = 'NonExistentResource' + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Add', + logicalResourceId: 'DifferentResource', + afterContext: '{"Type": "AWS::S3::Bucket"}', + }, + }, + ] + + const mockDocument = { + getText: () => '{\n "DifferentResource": {\n "Type": "AWS::S3::Bucket"\n }\n}', + } + openTextDocumentStub.resolves(mockDocument) + + await DiffViewHelper.openDiff(stackName, changes, resourceId) + + // Should only call diff once (without selection) + assert.ok(executeCommandStub.calledOnce) + }) + + it('should handle details with missing BeforeValue/AfterValue', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Modify', + logicalResourceId: 'ModifiedResource', + details: [ + { + Target: { + Name: 'Property1', + // Missing BeforeValue and AfterValue + }, + }, + ], + }, + }, + ] + + await testDiffGeneration(stackName, changes) + + const { beforeData, afterData } = assertFileCallsAndParseData() + + assert.strictEqual(beforeData.ModifiedResource.Property1, '') + assert.strictEqual(afterData.ModifiedResource.Property1, '') + }) + + it('should handle empty changes array', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [] + + await DiffViewHelper.openDiff(stackName, changes) + + assert.ok(writeFileStub.calledTwice) + assert.ok(writeFileStub.calledWith(sinon.match.any, '{}')) + assert.ok(executeCommandStub.calledOnce) + }) + }) + + describe('drift decorations', function () { + let createTextEditorDecorationTypeStub: sinon.SinonStub + let setDecorationsStub: sinon.SinonStub + let clock: sinon.SinonFakeTimers + + beforeEach(function () { + createTextEditorDecorationTypeStub = sandbox.stub(vscode.window, 'createTextEditorDecorationType') + setDecorationsStub = sandbox.stub() + clock = sandbox.useFakeTimers() + }) + + function setupMockEditor(stackName: string, documentText: string) { + const tmpDir = os.tmpdir() + const beforePath = path.join(tmpDir, `${stackName}-before.json`) + const beforeUri = vscode.Uri.file(beforePath).toString() + + const mockEditor = { + document: { + uri: { toString: () => beforeUri }, + getText: () => documentText, + }, + setDecorations: setDecorationsStub, + } + + sandbox.stub(vscode.window, 'visibleTextEditors').get(() => [mockEditor]) + } + + async function runDriftTest(stackName: string, changes: StackChange[]) { + await DiffViewHelper.openDiff(stackName, changes) + clock.tick(500) + } + + function assertDecorationCount(expectedCount: number) { + assert.ok(setDecorationsStub.called) + const decorations = setDecorationsStub.getCall(0).args[1] + assert.strictEqual(decorations.length, expectedCount) + return decorations + } + + function createDriftChange( + logicalResourceId: string, + beforeContext: string, + afterContext: string, + details: any[] + ): StackChange { + return { + resourceChange: { + action: 'Modify', + logicalResourceId, + beforeContext, + afterContext, + details, + }, + } + } + + function createDetailTarget( + name: string, + path: string, + beforeValue: string, + afterValue: string, + drift?: { PreviousValue: string; ActualValue: string } + ) { + return { + Target: { + Name: name, + Path: path, + BeforeValue: beforeValue, + AfterValue: afterValue, + ...(drift && { LiveResourceDrift: drift }), + }, + } + } + + it('should add drift decoration when LiveResourceDrift is present', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + createDriftChange( + 'MyQueue', + '{"Properties":{"DelaySeconds":"5"}}', + '{"Properties":{"DelaySeconds":"1"}}', + [ + createDetailTarget('DelaySeconds', '/Properties/DelaySeconds', '5', '1', { + PreviousValue: '1', + ActualValue: '5', + }), + ] + ), + ] + + setupMockEditor( + stackName, + '{\n "MyQueue": {\n "Properties": {\n "DelaySeconds": "5"\n }\n }\n}' + ) + await runDriftTest(stackName, changes) + + assert.ok(createTextEditorDecorationTypeStub.called) + const decorations = assertDecorationCount(1) + assert.ok(decorations[0].hoverMessage.includes('Resource Drift Detected')) + assert.ok(decorations[0].hoverMessage.includes('MyQueue')) + }) + + it('should not add decoration when LiveResourceDrift is not present', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + createDriftChange( + 'MyQueue', + '{"Properties":{"DelaySeconds":"5"}}', + '{"Properties":{"DelaySeconds":"1"}}', + [createDetailTarget('DelaySeconds', '/Properties/DelaySeconds', '5', '1')] + ), + ] + + setupMockEditor( + stackName, + '{\n "MyQueue": {\n "Properties": {\n "DelaySeconds": "5"\n }\n }\n}' + ) + await runDriftTest(stackName, changes) + + assertDecorationCount(0) + }) + + it('should handle nested property paths correctly', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + createDriftChange( + 'MyResource', + '{"Properties":{"Config":{"Setting":"old"}}}', + '{"Properties":{"Config":{"Setting":"new"}}}', + [ + createDetailTarget('Setting', '/Properties/Config/Setting', 'old', 'new', { + PreviousValue: 'new', + ActualValue: 'old', + }), + ] + ), + ] + + setupMockEditor( + stackName, + '{\n "MyResource": {\n "Properties": {\n "Config": {\n "Setting": "old"\n }\n }\n }\n}' + ) + await runDriftTest(stackName, changes) + + const decorations = assertDecorationCount(1) + assert.ok(decorations[0].hoverMessage.includes('/Properties/Config/Setting')) + }) + + it('should handle multiple drift decorations for different properties', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + createDriftChange( + 'MyQueue', + '{"Properties":{"DelaySeconds":"5","MessageRetentionPeriod":"100"}}', + '{"Properties":{"DelaySeconds":"1","MessageRetentionPeriod":"200"}}', + [ + createDetailTarget('DelaySeconds', '/Properties/DelaySeconds', '5', '1', { + PreviousValue: '1', + ActualValue: '5', + }), + createDetailTarget( + 'MessageRetentionPeriod', + '/Properties/MessageRetentionPeriod', + '100', + '200', + { PreviousValue: '100', ActualValue: '150' } + ), + ] + ), + ] + + setupMockEditor( + stackName, + '{\n "MyQueue": {\n "Properties": {\n "DelaySeconds": "5",\n "MessageRetentionPeriod": "100"\n }\n }\n}' + ) + await runDriftTest(stackName, changes) + + assertDecorationCount(2) + }) + + it('should add drift decoration for DELETED resources', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + { + resourceChange: { + logicalResourceId: 'DeletedResource', + resourceDriftStatus: 'DELETED', + }, + }, + ] + + setupMockEditor(stackName, '{\n "DeletedResource": {}\n}') + await runDriftTest(stackName, changes) + + const decorations = assertDecorationCount(1) + assert.ok(decorations[0].hoverMessage.includes('Resource Deleted')) + assert.ok(decorations[0].hoverMessage.includes('deleted sometime after the previous deployment')) + }) + + it('should handle array indices in property paths', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + createDriftChange( + 'MyRole', + '{"Properties":{"Policies":[{"PolicyDocument":"old"}]}}', + '{"Properties":{"Policies":[{"PolicyDocument":"new"}]}}', + [ + createDetailTarget('PolicyDocument', '/Properties/Policies/0/PolicyDocument', 'old', 'new', { + PreviousValue: 'old', + ActualValue: 'drifted', + }), + ] + ), + ] + + setupMockEditor( + stackName, + '{\n "MyRole": {\n "Properties": {\n "Policies": [\n {\n "PolicyDocument": "old"\n }\n ]\n }\n }\n}' + ) + await runDriftTest(stackName, changes) + + const decorations = assertDecorationCount(1) + assert.ok(decorations[0].hoverMessage.includes('/Properties/Policies/0/PolicyDocument')) + }) + + it('should not add decoration when property is not in afterContext', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Modify', + logicalResourceId: 'MyQueue', + beforeContext: '{"Properties":{"DelaySeconds":"5","MessageRetentionPeriod":"100"}}', + afterContext: '{"Properties":{"MessageRetentionPeriod":"200"}}', + details: [ + { + Target: { + Name: 'DelaySeconds', + Path: '/Properties/DelaySeconds', + BeforeValue: '5', + AfterValue: '1', + Drift: { + PreviousValue: '1', + ActualValue: '5', + }, + }, + }, + ], + }, + }, + ] + + setupMockEditor( + stackName, + '{\n "MyQueue": {\n "Properties": {\n "DelaySeconds": "5",\n "MessageRetentionPeriod": "100"\n }\n }\n}' + ) + await runDriftTest(stackName, changes) + + assertDecorationCount(0) + }) + + it('should not add decoration when ActualValue is undefined', async function () { + const stackName = 'test-stack' + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Modify', + logicalResourceId: 'MyQueue', + beforeContext: '{"Properties":{"DelaySeconds":"5"}}', + afterContext: '{"Properties":{"DelaySeconds":"1"}}', + details: [ + { + Target: { + Name: 'DelaySeconds', + Path: '/Properties/DelaySeconds', + AfterValue: '1', + Drift: { + PreviousValue: '1', + }, + }, + }, + ], + }, + }, + ] + + setupMockEditor( + stackName, + '{\n "MyQueue": {\n "Properties": {\n "DelaySeconds": "5"\n }\n }\n}' + ) + await runDriftTest(stackName, changes) + + assertDecorationCount(0) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/ui/diffWebviewProvider.test.ts b/packages/core/src/test/awsService/cloudformation/ui/diffWebviewProvider.test.ts new file mode 100644 index 00000000000..ff87dbf72d3 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/ui/diffWebviewProvider.test.ts @@ -0,0 +1,556 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { DiffWebviewProvider } from '../../../../awsService/cloudformation/ui/diffWebviewProvider' +import { + DeploymentMode, + StackChange, +} from '../../../../awsService/cloudformation/stacks/actions/stackActionRequestType' +import { ChangeSetStatus } from '@aws-sdk/client-cloudformation' + +describe('DiffWebviewProvider', function () { + let sandbox: sinon.SinonSandbox + let provider: DiffWebviewProvider + + beforeEach(function () { + sandbox = sinon.createSandbox() + const mockCoordinator = { + onDidChangeStack: sandbox.stub().returns({ dispose: () => {} }), + setChangeSetMode: sandbox.stub().resolves(), + } as any + provider = new DiffWebviewProvider(mockCoordinator) + }) + + afterEach(function () { + sandbox.restore() + }) + + function createMockWebview() { + return { + webview: { + options: {}, + html: '', + onDidReceiveMessage: sandbox.stub(), + }, + } + } + + function setupProviderWithChanges(stackName: string, changes: StackChange[]) { + void provider.updateData(stackName, changes) + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + return mockWebview.webview.html + } + + describe('updateData', function () { + it('should update stack name and changes', function () { + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Add', + logicalResourceId: 'TestResource', + resourceType: 'AWS::S3::Bucket', + }, + }, + ] + + const html = setupProviderWithChanges('test-stack', changes) + + // The HTML should contain the resource information (stack name doesn't appear in table HTML) + assert.ok(html.includes('TestResource')) + assert.ok(html.includes('Add')) + assert.ok(html.includes('AWS::S3::Bucket')) + // Verify it's not the "No changes detected" message + assert.ok(!html.includes('No changes detected')) + }) + + it('should handle empty changes array', function () { + const html = setupProviderWithChanges('empty-stack', []) + assert.ok(html.includes('No changes detected')) + assert.ok(html.includes('empty-stack')) + }) + }) + + describe('resolveWebviewView', function () { + it('should configure webview options and set HTML content', function () { + const mockWebview = createMockWebview() + + void provider.updateData('test-stack', []) + provider.resolveWebviewView(mockWebview as any) + + assert.deepStrictEqual(mockWebview.webview.options, { enableScripts: true }) + assert.ok(mockWebview.webview.html.length > 0) + assert.ok(mockWebview.webview.onDidReceiveMessage.calledOnce) + }) + }) + + describe('HTML generation', function () { + it('should generate table with correct columns for changes with details', function () { + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Modify', + logicalResourceId: 'TestBucket', + physicalResourceId: 'test-bucket-123', + resourceType: 'AWS::S3::Bucket', + replacement: 'False', + scope: ['Properties'], + details: [ + { + Target: { + Name: 'BucketName', + RequiresRecreation: 'Never', + BeforeValue: 'old-bucket', + AfterValue: 'new-bucket', + AttributeChangeType: 'Modify', + }, + ChangeSource: 'DirectModification', + CausingEntity: 'user-change', + }, + ], + }, + }, + ] + + const html = setupProviderWithChanges('test-stack', changes) + + // Verify main table headers + assert.ok(html.includes('Action')) + assert.ok(html.includes('LogicalResourceId')) + assert.ok(html.includes('ResourceType')) + assert.ok(html.includes('Replacement')) + + // Verify main row data + assert.ok(html.includes('Modify')) + assert.ok(html.includes('TestBucket')) + assert.ok(html.includes('test-bucket-123')) + assert.ok(html.includes('AWS::S3::Bucket')) + + // Verify detail data (in expandable section) + assert.ok(html.includes('BucketName')) + assert.ok(html.includes('old-bucket')) + assert.ok(html.includes('new-bucket')) + assert.ok(html.includes('DirectModification')) + assert.ok(html.includes('user-change')) + + // Verify expandable structure + assert.ok(html.includes('toggleDetails')) + assert.ok(html.includes('display: none')) + assert.ok(html.includes(' ({ + resourceChange: { + action: 'Add', + logicalResourceId: `Resource${i}`, + resourceType: 'AWS::S3::Bucket', + }, + })) + + const html = setupProviderWithChanges('test-stack', changes) + + assert.ok(html.includes('Page 1 of 2')) + assert.ok(html.includes('nextPage()')) + assert.ok(html.includes('prevPage()')) + assert.ok(html.includes('pagination-controls')) + }) + + it('should not show pagination for small change sets', function () { + const changes: StackChange[] = [ + { + resourceChange: { + action: 'Add', + logicalResourceId: 'SingleResource', + resourceType: 'AWS::S3::Bucket', + }, + }, + ] + + const html = setupProviderWithChanges('test-stack', changes) + + assert.ok(!html.includes('pagination-controls')) + assert.ok(!html.includes('Page 1 of')) + }) + + it('should display correct page numbers and navigation state', function () { + const changes: StackChange[] = Array.from({ length: 150 }, (_, i) => ({ + resourceChange: { + action: 'Add', + logicalResourceId: `Resource${i}`, + }, + })) + + const html = setupProviderWithChanges('test-stack', changes) + + assert.ok(html.includes('Page 1 of 3')) + // Previous button should be disabled on first page + assert.ok(html.includes('opacity: 0.5')) + assert.ok(html.includes('cursor: not-allowed')) + }) + }) + + describe('empty changes handling', function () { + it('should show no changes message when changes is undefined', function () { + void provider.updateData( + 'test-stack', + undefined as any, + 'test-changeset', + undefined, + undefined, + undefined, + ChangeSetStatus.FAILED + ) + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + + assert.ok(mockWebview.webview.html.includes('No changes detected')) + assert.ok(mockWebview.webview.html.includes('test-stack')) + assert.ok(mockWebview.webview.html.includes('Delete Changeset')) + }) + + it('should show no changes message when changes array is empty', function () { + void provider.updateData( + 'empty-stack', + [], + 'test-changeset', + undefined, + undefined, + undefined, + ChangeSetStatus.FAILED + ) + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + const html = mockWebview.webview.html + + assert.ok(html.includes('No changes detected')) + assert.ok(html.includes('empty-stack')) + assert.ok(html.includes('Delete Changeset')) + }) + + it('should not show delete button when no changeset status', function () { + void provider.updateData('empty-stack', [], 'test-changeset', undefined, undefined, undefined, undefined) + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + const html = mockWebview.webview.html + + assert.ok(html.includes('No changes detected')) + assert.ok(html.includes('empty-stack')) + assert.ok(!html.includes('Delete Changeset')) + }) + + it('should not show table when no changes', function () { + const html = setupProviderWithChanges('empty-stack', []) + + assert.ok(!html.includes(' { + let sandbox: sinon.SinonSandbox + let provider: StackEventsWebviewProvider + let mockClient: any + let coordinatorCallback: any + + function createMockView() { + return { + webview: { + onDidReceiveMessage: sandbox.stub(), + html: '', + options: {}, + }, + onDidChangeVisibility: sandbox.stub(), + visible: true, + onDidDispose: sandbox.stub(), + } + } + + beforeEach(() => { + sandbox = sinon.createSandbox() + mockClient = { + sendRequest: sandbox.stub().resolves({ + events: [ + { + EventId: 'event-1', + StackName: 'test-stack', + Timestamp: new Date(), + ResourceStatus: 'CREATE_IN_PROGRESS', + }, + ], + nextToken: undefined, + }), + } + const mockCoordinator: any = { + onDidChangeStack: sandbox.stub().callsFake((callback: any) => { + coordinatorCallback = callback + return { dispose: () => {} } + }), + } + provider = new StackEventsWebviewProvider(mockClient, mockCoordinator) + }) + + afterEach(() => { + provider.dispose() + sandbox.restore() + }) + + it('should load stack events', async () => { + await provider.showStackEvents('test-stack') + + assert.strictEqual(mockClient.sendRequest.calledOnce, true) + }) + + it('should stop auto-refresh on terminal state', async () => { + const clock = sandbox.useFakeTimers() + + await provider.showStackEvents('test-stack') + + // Simulate terminal state notification + await coordinatorCallback({ + stackName: 'test-stack', + isChangeSetMode: false, + stackStatus: 'CREATE_COMPLETE', + }) + + clock.tick(10000) + + // Should not continue refreshing after terminal state + const callCount = mockClient.sendRequest.callCount + clock.tick(5000) + assert.strictEqual(mockClient.sendRequest.callCount, callCount) + + clock.restore() + }) + + it('should continue auto-refresh during in-progress state', async () => { + const clock = sandbox.useFakeTimers() + + await provider.showStackEvents('test-stack') + + await coordinatorCallback({ + stackName: 'test-stack', + isChangeSetMode: false, + stackStatus: 'UPDATE_IN_PROGRESS', + }) + + const initialCalls = mockClient.sendRequest.callCount + clock.tick(5000) + + assert.strictEqual(mockClient.sendRequest.callCount > initialCalls, true) + + clock.restore() + }) + + it('should include console link with ARN when stackArn is set', async () => { + const view = createMockView() + provider.resolveWebviewView(view as any) + + await coordinatorCallback({ + stackName: 'test-stack', + stackArn: 'arn:aws:cloudformation:us-west-2:123456789012:stack/test-stack/xyz-456', + isChangeSetMode: false, + }) + + const html = view.webview.html + assert.ok(html.includes('href="https://us-west-2.console.aws.amazon.com')) + assert.ok(html.includes('/stacks/events?stackId=')) + assert.ok(html.includes('View in AWS Console')) + }) + + it('should not include console link when stackArn is missing', async () => { + const view = createMockView() + provider.resolveWebviewView(view as any) + + await coordinatorCallback({ + stackName: 'test-stack', + stackArn: undefined, + isChangeSetMode: false, + }) + + const html = view.webview.html + assert.ok(!html.includes('href="https://')) + }) + + it('should show "X events loaded" in header when nextToken is available', async () => { + mockClient.sendRequest.resolves({ + events: Array.from({ length: 50 }, (_, i) => ({ + EventId: `event-${i}`, + StackName: 'test-stack', + Timestamp: new Date(), + ResourceStatus: 'CREATE_IN_PROGRESS', + })), + nextToken: 'token123', + }) + + const view = createMockView() + provider.resolveWebviewView(view as any) + await provider.showStackEvents('test-stack') + + const html = view.webview.html + assert.ok(html.includes('(50 events loaded)')) + }) + + it('should show "X events" in header when nextToken is not available', async () => { + mockClient.sendRequest.resolves({ + events: Array.from({ length: 50 }, (_, i) => ({ + EventId: `event-${i}`, + StackName: 'test-stack', + Timestamp: new Date(), + ResourceStatus: 'CREATE_IN_PROGRESS', + })), + nextToken: undefined, + }) + + const view = createMockView() + provider.resolveWebviewView(view as any) + await provider.showStackEvents('test-stack') + + const html = view.webview.html + assert.ok(html.includes('(50 events)')) + assert.ok(!html.includes('(50 events loaded)')) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/ui/stackOutputsWebviewProvider.test.ts b/packages/core/src/test/awsService/cloudformation/ui/stackOutputsWebviewProvider.test.ts new file mode 100644 index 00000000000..db5c0e3ab4c --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/ui/stackOutputsWebviewProvider.test.ts @@ -0,0 +1,121 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { StackOutputsWebviewProvider } from '../../../../awsService/cloudformation/ui/stackOutputsWebviewProvider' + +describe('StackOutputsWebviewProvider', () => { + let sandbox: sinon.SinonSandbox + let provider: StackOutputsWebviewProvider + let mockClient: any + let mockCoordinator: any + + function createMockView() { + return { + webview: { + options: {}, + html: '', + }, + } + } + + beforeEach(() => { + sandbox = sinon.createSandbox() + mockClient = { + sendRequest: sandbox.stub().resolves({ + stack: { + StackName: 'test-stack', + StackStatus: 'CREATE_COMPLETE', + Outputs: [ + { + OutputKey: 'BucketName', + OutputValue: 'my-bucket', + Description: 'S3 bucket name', + }, + ], + }, + }), + } + mockCoordinator = { + onDidChangeStack: sandbox.stub().returns({ dispose: () => {} }), + setStack: sandbox.stub().resolves(), + currentStackStatus: undefined, + } + provider = new StackOutputsWebviewProvider(mockClient, mockCoordinator) + }) + + afterEach(() => { + provider.dispose() + sandbox.restore() + }) + + it('should use DescribeStackRequest to load outputs', async () => { + await provider.resolveWebviewView(createMockView() as any) + await provider.showOutputs('test-stack') + + assert.strictEqual(mockClient.sendRequest.calledOnce, true) + const requestArgs = mockClient.sendRequest.firstCall.args + assert.strictEqual(requestArgs[1].stackName, 'test-stack') + }) + + it('should extract outputs from stack object', async () => { + const mockView = createMockView() + await provider.resolveWebviewView(mockView as any) + + await provider.showOutputs('test-stack') + + assert.strictEqual(mockView.webview.html.includes('BucketName'), true) + assert.strictEqual(mockView.webview.html.includes('my-bucket'), true) + }) + + it('should update coordinator with stack status', async () => { + await provider.resolveWebviewView(createMockView() as any) + await provider.showOutputs('test-stack') + + assert.strictEqual(mockCoordinator.setStack.calledWith('test-stack', 'CREATE_COMPLETE'), true) + }) + + it('should not update coordinator if status unchanged', async () => { + mockCoordinator.currentStackStatus = 'CREATE_COMPLETE' + + await provider.resolveWebviewView(createMockView() as any) + await provider.showOutputs('test-stack') + + assert.strictEqual(mockCoordinator.setStack.called, false) + }) + + it('should include console link with ARN when stackArn is set', async () => { + const mockView = createMockView() + await provider.resolveWebviewView(mockView as any) + + const coordinatorCallback = mockCoordinator.onDidChangeStack.firstCall.args[0] + await coordinatorCallback({ + stackName: 'test-stack', + stackArn: 'arn:aws:cloudformation:eu-west-1:123456789012:stack/test-stack/def-789', + isChangeSetMode: false, + }) + + const html = mockView.webview.html + assert.ok(html.includes('href="https://eu-west-1.console.aws.amazon.com')) + assert.ok(html.includes('/stacks/outputs?stackId=')) + assert.ok(html.includes('View in AWS Console')) + }) + + it('should not include console link when stackArn is missing', async () => { + const mockView = createMockView() + await provider.resolveWebviewView(mockView as any) + + const coordinatorCallback = mockCoordinator.onDidChangeStack.firstCall.args[0] + await coordinatorCallback({ + stackName: 'test-stack', + stackArn: undefined, + isChangeSetMode: false, + }) + + const html = mockView.webview.html + assert.ok(!html.includes('href="https://')) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/ui/stackOverviewWebviewProvider.test.ts b/packages/core/src/test/awsService/cloudformation/ui/stackOverviewWebviewProvider.test.ts new file mode 100644 index 00000000000..a835c3cb87d --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/ui/stackOverviewWebviewProvider.test.ts @@ -0,0 +1,141 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { StackOverviewWebviewProvider } from '../../../../awsService/cloudformation/ui/stackOverviewWebviewProvider' + +describe('StackOverviewWebviewProvider', () => { + let sandbox: sinon.SinonSandbox + let provider: StackOverviewWebviewProvider + let mockClient: any + let mockCoordinator: any + let coordinatorCallback: any + + function createMockView() { + return { + webview: { + options: {}, + html: '', + }, + onDidChangeVisibility: sandbox.stub(), + onDidDispose: sandbox.stub(), + visible: true, + } + } + + beforeEach(() => { + sandbox = sinon.createSandbox() + mockClient = { + sendRequest: sandbox.stub().resolves({ + stack: { + StackName: 'test-stack', + StackStatus: 'CREATE_COMPLETE', + StackId: 'stack-id-123', + CreationTime: new Date(), + }, + }), + } + mockCoordinator = { + onDidChangeStack: sandbox.stub().callsFake((callback: any) => { + coordinatorCallback = callback + return { dispose: () => {} } + }), + setStack: sandbox.stub().resolves(), + currentStackStatus: undefined, + } + provider = new StackOverviewWebviewProvider(mockClient, mockCoordinator) + }) + + afterEach(() => { + provider.dispose() + sandbox.restore() + }) + + it('should load stack overview', async () => { + provider.resolveWebviewView(createMockView() as any) + await provider.showStackOverview('test-stack') + + assert.strictEqual(mockClient.sendRequest.calledOnce, true) + assert.strictEqual(mockCoordinator.setStack.calledOnce, true) + }) + + it('should update coordinator with stack status', async () => { + provider.resolveWebviewView(createMockView() as any) + await provider.showStackOverview('test-stack') + + assert.strictEqual(mockCoordinator.setStack.calledWith('test-stack', 'CREATE_COMPLETE'), true) + }) + + it('should not update coordinator if status unchanged', async () => { + mockCoordinator.currentStackStatus = 'CREATE_COMPLETE' + + provider.resolveWebviewView(createMockView() as any) + await provider.showStackOverview('test-stack') + + assert.strictEqual(mockCoordinator.setStack.called, false) + }) + + it('should start auto-refresh on stack change', async () => { + const clock = sandbox.useFakeTimers() + + await coordinatorCallback({ + stackName: 'test-stack', + isChangeSetMode: false, + stackStatus: 'CREATE_IN_PROGRESS', + }) + + clock.tick(5000) + + assert.strictEqual(mockClient.sendRequest.callCount >= 2, true) + + clock.restore() + }) + + it('should stop auto-refresh on terminal state', async () => { + const clock = sandbox.useFakeTimers() + + await coordinatorCallback({ + stackName: 'test-stack', + isChangeSetMode: false, + stackStatus: 'CREATE_COMPLETE', + }) + + clock.tick(10000) + + // Should only be called once (initial load), not refreshed + assert.strictEqual(mockClient.sendRequest.callCount, 1) + + clock.restore() + }) + + it('should include console link with ARN in HTML', async () => { + const view = createMockView() + provider.resolveWebviewView(view as any) + await provider.showStackOverview('test-stack') + + const html = view.webview.html + assert.ok(html.includes('href="https://console.aws.amazon.com/go/view?arn=')) + assert.ok(html.includes('stack-id-123')) + assert.ok(html.includes('View in AWS Console')) + }) + + it('should not include console link when ARN is missing', async () => { + mockClient.sendRequest.resolves({ + stack: { + StackName: 'test-stack', + StackStatus: 'CREATE_COMPLETE', + StackId: undefined, + }, + }) + + const view = createMockView() + provider.resolveWebviewView(view as any) + await provider.showStackOverview('test-stack') + + const html = view.webview.html + assert.ok(!html.includes('href="https://')) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/ui/stackResourcesWebviewProvider.test.ts b/packages/core/src/test/awsService/cloudformation/ui/stackResourcesWebviewProvider.test.ts new file mode 100644 index 00000000000..effe7e2e3c2 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/ui/stackResourcesWebviewProvider.test.ts @@ -0,0 +1,354 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { StackResourcesWebviewProvider } from '../../../../awsService/cloudformation/ui/stackResourcesWebviewProvider' + +describe('StackResourcesWebviewProvider', function () { + let sandbox: sinon.SinonSandbox + let provider: StackResourcesWebviewProvider + let mockClient: any + let mockCoordinator: any + + beforeEach(function () { + sandbox = sinon.createSandbox() + mockClient = { + sendRequest: sandbox.stub(), + } + mockCoordinator = { + onDidChangeStack: sandbox.stub().returns({ dispose: () => {} }), + setStack: sandbox.stub().resolves(), + currentStackStatus: undefined, + } as any + provider = new StackResourcesWebviewProvider(mockClient, mockCoordinator) + }) + + afterEach(function () { + sandbox.restore() + }) + + function createMockWebview() { + return { + webview: { + options: {}, + html: '', + onDidReceiveMessage: sandbox.stub(), + }, + onDidChangeVisibility: sandbox.stub(), + onDidDispose: sandbox.stub(), + visible: true, + } + } + + function createMockResources(count: number, startIndex = 0) { + return Array.from({ length: count }, (_, i) => ({ + LogicalResourceId: `Resource${i + startIndex}`, + PhysicalResourceId: `resource-${i + startIndex}-123`, + ResourceType: 'AWS::S3::Bucket', + ResourceStatus: 'CREATE_COMPLETE', + })) + } + + async function setupProviderWithResources(stackName: string, resources: any[], nextToken?: string) { + mockClient.sendRequest.resolves({ resources, nextToken }) + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + await provider.updateData(stackName) + return mockWebview + } + + describe('updateData', function () { + it('should update stack name and fetch resources', async function () { + const mockResources = createMockResources(1) + mockClient.sendRequest.resolves({ resources: mockResources }) + + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + await provider.updateData('test-stack') + + assert.ok(mockClient.sendRequest.calledOnce) + const [, params] = mockClient.sendRequest.firstCall.args + assert.strictEqual(params.stackName, 'test-stack') + }) + + it('should handle client request errors gracefully', async function () { + mockClient.sendRequest.rejects(new Error('Network error')) + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + + // Should not throw + await provider.updateData('test-stack') + }) + }) + + describe('resolveWebviewView', function () { + it('should configure webview options and set HTML content', function () { + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + + assert.deepStrictEqual(mockWebview.webview.options, { enableScripts: true }) + assert.ok(mockWebview.webview.html.length > 0) + }) + + it('should set up visibility change handlers', function () { + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + + assert.ok(mockWebview.onDidChangeVisibility.calledOnce) + assert.ok(mockWebview.onDidDispose.calledOnce) + }) + + it('should set up message handlers for pagination', function () { + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + + assert.ok(mockWebview.webview.onDidReceiveMessage.calledOnce) + }) + }) + + describe('HTML generation', function () { + it('should show no resources message when empty', async function () { + const mockWebview = await setupProviderWithResources('test-stack', []) + assert.ok(mockWebview.webview.html.includes('No resources found')) + }) + + it('should generate table with resources', async function () { + const mockResources = [ + { + LogicalResourceId: 'TestBucket', + PhysicalResourceId: 'test-bucket-123', + ResourceType: 'AWS::S3::Bucket', + ResourceStatus: 'CREATE_COMPLETE', + }, + ] + + const mockWebview = await setupProviderWithResources('test-stack', mockResources) + const html = mockWebview.webview.html + + // Verify table headers and data + assert.ok(html.includes('Logical ID')) + assert.ok(html.includes('Physical ID')) + assert.ok(html.includes('Type')) + assert.ok(html.includes('Status')) + assert.ok(html.includes('TestBucket')) + assert.ok(html.includes('test-bucket-123')) + assert.ok(html.includes('AWS::S3::Bucket')) + assert.ok(html.includes('CREATE_COMPLETE')) + }) + + it('should handle resources without physical ID', async function () { + const mockResources = [ + { + LogicalResourceId: 'TestResource', + ResourceType: 'AWS::CloudFormation::WaitConditionHandle', + ResourceStatus: 'CREATE_COMPLETE', + }, + ] + + const mockWebview = await setupProviderWithResources('test-stack', mockResources) + const html = mockWebview.webview.html + + assert.ok(html.includes('TestResource')) + assert.ok(html.includes('AWS::CloudFormation::WaitConditionHandle')) + assert.ok(html.includes('CREATE_COMPLETE')) + }) + + it('should show pagination controls with buttons disabled when there is only one page', async function () { + const mockWebview = await setupProviderWithResources('test-stack', createMockResources(10)) + const html = mockWebview.webview.html + + // Pagination is always shown, but buttons should be disabled for single page + assert.ok(html.includes('Previous')) + assert.ok(html.includes('Next')) + assert.ok(html.includes('disabled')) + }) + + it('should show pagination controls when there are multiple pages', async function () { + const mockWebview = await setupProviderWithResources('test-stack', createMockResources(60)) + const html = mockWebview.webview.html + + // Should show pagination buttons for multiple pages + assert.ok(html.includes('Previous')) + assert.ok(html.includes('Next')) + }) + + it('should disable Previous button on first page', async function () { + const mockWebview = await setupProviderWithResources('test-stack', createMockResources(60)) + const html = mockWebview.webview.html + + // Previous button should be disabled on first page + assert.ok(html.includes('disabled')) + assert.ok(html.includes('Previous')) + }) + + it('should include console link with ARN when stackArn is set', async function () { + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + + const coordinatorCallback = mockCoordinator.onDidChangeStack.firstCall.args[0] + await coordinatorCallback({ + stackName: 'test-stack', + stackArn: 'arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc-123', + isChangeSetMode: false, + }) + + const html = mockWebview.webview.html + assert.ok(html.includes('href="https://us-east-1.console.aws.amazon.com')) + assert.ok(html.includes('/stacks/resources?stackId=')) + assert.ok(html.includes('View in AWS Console')) + }) + + it('should not include console link when stackArn is missing', async function () { + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + + const coordinatorCallback = mockCoordinator.onDidChangeStack.firstCall.args[0] + await coordinatorCallback({ + stackName: 'test-stack', + stackArn: undefined, + isChangeSetMode: false, + }) + + const html = mockWebview.webview.html + assert.ok(!html.includes('href="https://')) + }) + + it('should show "X resources loaded" in header when nextToken is available', async function () { + const mockWebview = await setupProviderWithResources('test-stack', createMockResources(50), 'token123') + const html = mockWebview.webview.html + assert.ok(html.includes('(50 resources loaded)')) + }) + + it('should show "X resources" in header when nextToken is not available', async function () { + const mockWebview = await setupProviderWithResources('test-stack', createMockResources(60)) + const html = mockWebview.webview.html + assert.ok(html.includes('(60 resources)')) + assert.ok(!html.includes('(60 resources loaded)')) + }) + }) + + describe('pagination functionality', function () { + let clock: sinon.SinonFakeTimers + + beforeEach(function () { + clock = sandbox.useFakeTimers() + }) + + afterEach(function () { + clock.restore() + }) + + async function testPaginationMessage(command: string) { + const mockWebview = await setupProviderWithResources('test-stack', createMockResources(60)) + const messageHandler = mockWebview.webview.onDidReceiveMessage.firstCall.args[0] + await messageHandler({ command }) + assert.ok(mockWebview.webview.html.length > 0) + } + + it('should handle nextPage message', async function () { + await testPaginationMessage('nextPage') + }) + + it('should handle prevPage message', async function () { + await testPaginationMessage('prevPage') + }) + + it('should start auto-update when webview becomes visible', async function () { + mockCoordinator.currentStackStatus = 'UPDATE_IN_PROGRESS' + const mockWebview = await setupProviderWithResources('test-stack', []) + const visibilityHandler = mockWebview.onDidChangeVisibility.firstCall.args[0] + mockWebview.visible = true + visibilityHandler() + + const initialCallCount = mockClient.sendRequest.callCount + clock.tick(5000) + + assert.ok(mockClient.sendRequest.callCount >= initialCallCount + 1) + }) + + it('should stop auto-update when webview becomes hidden', async function () { + const mockWebview = await setupProviderWithResources('test-stack', []) + const visibilityHandler = mockWebview.onDidChangeVisibility.firstCall.args[0] + + // Start then stop auto-update + mockWebview.visible = true + visibilityHandler() + mockWebview.visible = false + visibilityHandler() + + const callCountAfterStop = mockClient.sendRequest.callCount + clock.tick(10000) + assert.strictEqual(mockClient.sendRequest.callCount, callCountAfterStop) + }) + }) + + describe('loadResources', function () { + async function setupPaginatedTest() { + const firstBatch = createMockResources(50) + const secondBatch = createMockResources(10, 50) + + mockClient.sendRequest + .onFirstCall() + .resolves({ resources: firstBatch, nextToken: 'token123' }) + .onSecondCall() + .resolves({ resources: secondBatch }) + + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + await provider.updateData('test-stack') + + return mockWebview + } + + it('should handle nextToken for pagination', async function () { + const mockWebview = await setupPaginatedTest() + const messageHandler = mockWebview.webview.onDidReceiveMessage.firstCall.args[0] + await messageHandler({ command: 'nextPage' }) + + assert.strictEqual(mockClient.sendRequest.callCount, 2) + }) + + it('should return early if no client or stack name', async function () { + const mockCoordinator = { + onDidChangeStack: sandbox.stub().returns({ dispose: () => {} }), + } as any + const providerWithoutClient = new StackResourcesWebviewProvider(undefined as any, mockCoordinator) + const mockWebview = createMockWebview() + providerWithoutClient.resolveWebviewView(mockWebview as any) + + await providerWithoutClient.updateData('') + }) + + it('should not duplicate resources when updateData is called multiple times', async function () { + const mockResources = createMockResources(5) + mockClient.sendRequest.resolves({ resources: mockResources }) + + const mockWebview = createMockWebview() + provider.resolveWebviewView(mockWebview as any) + await provider.updateData('test-stack') + await provider.updateData('test-stack') + + const html = mockWebview.webview.html + const resource0Count = (html.match(/Resource0/g) || []).length + assert.strictEqual(resource0Count, 1) + }) + + it('should overwrite resources on initial load and append on subsequent paginated loads', async function () { + const mockWebview = await setupPaginatedTest() + + let html = mockWebview.webview.html + assert.ok(html.includes('Resource0')) + assert.ok(html.includes('Resource49')) + + const messageHandler = mockWebview.webview.onDidReceiveMessage.firstCall.args[0] + await messageHandler({ command: 'nextPage' }) + + html = mockWebview.webview.html + assert.ok(html.includes('Resource50')) + assert.ok(html.includes('Resource59')) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/ui/stackViewCoordinator.test.ts b/packages/core/src/test/awsService/cloudformation/ui/stackViewCoordinator.test.ts new file mode 100644 index 00000000000..8f307010d1b --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/ui/stackViewCoordinator.test.ts @@ -0,0 +1,103 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { StackViewCoordinator } from '../../../../awsService/cloudformation/ui/stackViewCoordinator' + +describe('StackViewCoordinator', () => { + let coordinator: StackViewCoordinator + + beforeEach(() => { + coordinator = new StackViewCoordinator() + }) + + afterEach(() => { + coordinator.dispose() + }) + + it('should initialize with undefined state', () => { + assert.strictEqual(coordinator.currentStackName, undefined) + assert.strictEqual(coordinator.currentStackStatus, undefined) + assert.strictEqual(coordinator.isChangeSetMode, false) + }) + + it('should set stack name and status', async () => { + await coordinator.setStack('test-stack', 'CREATE_COMPLETE') + + assert.strictEqual(coordinator.currentStackName, 'test-stack') + assert.strictEqual(coordinator.currentStackStatus, 'CREATE_COMPLETE') + assert.strictEqual(coordinator.isChangeSetMode, false) + }) + + it('should fire event when stack changes', async () => { + let eventFired = false + let receivedState: any + + coordinator.onDidChangeStack((state) => { + eventFired = true + receivedState = state + }) + + await coordinator.setStack('test-stack', 'CREATE_IN_PROGRESS') + + assert.strictEqual(eventFired, true) + assert.strictEqual(receivedState.stackName, 'test-stack') + assert.strictEqual(receivedState.stackStatus, 'CREATE_IN_PROGRESS') + assert.strictEqual(receivedState.isChangeSetMode, false) + }) + + it('should call status update callback when status changes', async () => { + let callbackCount = 0 + let receivedStackName: string | undefined + let receivedStatus: string | undefined + + coordinator.setStackStatusUpdateCallback((stackName, status) => { + callbackCount++ + receivedStackName = stackName + receivedStatus = status + }) + + await coordinator.setStack('test-stack', 'CREATE_COMPLETE') + + assert.strictEqual(callbackCount, 1) + assert.strictEqual(receivedStackName, 'test-stack') + assert.strictEqual(receivedStatus, 'CREATE_COMPLETE') + + await coordinator.setStack('test-stack', 'UPDATE_IN_PROGRESS') + + assert.strictEqual(callbackCount, 2) + assert.strictEqual(receivedStatus, 'UPDATE_IN_PROGRESS') + }) + + it('should not call callback if status unchanged', async () => { + let callbackCount = 0 + + coordinator.setStackStatusUpdateCallback(() => { + callbackCount++ + }) + + await coordinator.setStack('test-stack', 'CREATE_COMPLETE') + assert.strictEqual(callbackCount, 1) + + await coordinator.setStack('test-stack', 'CREATE_COMPLETE') + assert.strictEqual(callbackCount, 1) + }) + + it('should set change set mode', async () => { + await coordinator.setChangeSetMode('test-stack', true) + + assert.strictEqual(coordinator.currentStackName, 'test-stack') + assert.strictEqual(coordinator.isChangeSetMode, true) + }) + + it('should clear stack', async () => { + await coordinator.setStack('test-stack', 'CREATE_COMPLETE') + await coordinator.clearStack() + + assert.strictEqual(coordinator.currentStackName, undefined) + assert.strictEqual(coordinator.currentStackStatus, undefined) + assert.strictEqual(coordinator.isChangeSetMode, false) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/ui/statusBar.test.ts b/packages/core/src/test/awsService/cloudformation/ui/statusBar.test.ts new file mode 100644 index 00000000000..57aa75d0b5b --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/ui/statusBar.test.ts @@ -0,0 +1,162 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { createDeploymentStatusBar, updateWorkflowStatus } from '../../../../awsService/cloudformation/ui/statusBar' +import { StackActionPhase } from '../../../../awsService/cloudformation/stacks/actions/stackActionRequestType' +import { getTestWindow } from '../../../shared/vscode/window' + +describe('StatusBar', function () { + let sandbox: sinon.SinonSandbox + let clock: sinon.SinonFakeTimers + + beforeEach(function () { + sandbox = sinon.createSandbox() + clock = sandbox.useFakeTimers() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('createDeploymentStatusBar', function () { + it('creates status bar handle', function () { + const handle = createDeploymentStatusBar('stack1', 'Validation') + + assert.ok(handle) + assert.strictEqual(typeof handle.update, 'function') + assert.strictEqual(typeof handle.release, 'function') + + handle.release() + clock.tick(5000) + }) + + it('creates handle for deployment with changeset', function () { + const handle = createDeploymentStatusBar('stack1', 'Deployment', 'changeset1') + + assert.ok(handle) + + handle.release() + clock.tick(5000) + }) + + it('shows single operation with stack name', function () { + const handle = createDeploymentStatusBar('my-stack', 'Validation') + const statusBar = getTestWindow().statusBar + + const messages = statusBar.messages + assert.ok(messages.some((msg) => msg.includes('Validating my-stack'))) + + handle.release() + clock.tick(5000) + }) + + it('shows count for multiple operations', function () { + const handle1 = createDeploymentStatusBar('stack1', 'Validation') + const handle2 = createDeploymentStatusBar('stack2', 'Deployment', 'changeset1') + const statusBar = getTestWindow().statusBar + + const messages = statusBar.messages + assert.ok(messages.some((msg) => msg.includes('AWS CloudFormation (2)'))) + + handle1.release() + handle2.release() + clock.tick(5000) + }) + }) + + describe('updateWorkflowStatus', function () { + it('updates single operation display', function () { + const handle = createDeploymentStatusBar('my-stack', 'Validation') + const statusBar = getTestWindow().statusBar + + updateWorkflowStatus(handle, StackActionPhase.VALIDATION_IN_PROGRESS) + assert.ok(statusBar.messages.some((msg) => msg.includes('Validating my-stack'))) + + updateWorkflowStatus(handle, StackActionPhase.VALIDATION_COMPLETE) + assert.ok(statusBar.messages.some((msg) => msg.includes('Validated my-stack'))) + + handle.release() + clock.tick(5000) + }) + + it('shows failure for single operation', function () { + const handle = createDeploymentStatusBar('my-stack', 'Validation') + const statusBar = getTestWindow().statusBar + + updateWorkflowStatus(handle, StackActionPhase.VALIDATION_FAILED) + assert.ok(statusBar.messages.some((msg) => msg.includes('Validation Failed: my-stack'))) + + handle.release() + clock.tick(5000) + }) + + it('handles terminal phases', function () { + const handle = createDeploymentStatusBar('stack1', 'Validation') + + updateWorkflowStatus(handle, StackActionPhase.VALIDATION_COMPLETE) + handle.release() + + clock.tick(5000) + }) + + it('handles multiple concurrent operations', function () { + const handle1 = createDeploymentStatusBar('stack1', 'Validation') + const handle2 = createDeploymentStatusBar('stack2', 'Deployment', 'changeset1') + + updateWorkflowStatus(handle1, StackActionPhase.VALIDATION_COMPLETE) + updateWorkflowStatus(handle2, StackActionPhase.DEPLOYMENT_COMPLETE) + + handle1.release() + handle2.release() + + clock.tick(5000) + }) + + it('handles deployment operations', function () { + const handle = createDeploymentStatusBar('my-stack', 'Deployment', 'changeset1') + const statusBar = getTestWindow().statusBar + + updateWorkflowStatus(handle, StackActionPhase.DEPLOYMENT_IN_PROGRESS) + assert.ok(statusBar.messages.some((msg) => msg.includes('Deploying my-stack'))) + + updateWorkflowStatus(handle, StackActionPhase.DEPLOYMENT_COMPLETE) + assert.ok(statusBar.messages.some((msg) => msg.includes('Deployed my-stack'))) + + handle.release() + clock.tick(5000) + }) + + it('shows deployment failure', function () { + const handle = createDeploymentStatusBar('my-stack', 'Deployment', 'changeset1') + const statusBar = getTestWindow().statusBar + + updateWorkflowStatus(handle, StackActionPhase.DEPLOYMENT_FAILED) + assert.ok(statusBar.messages.some((msg) => msg.includes('Deployment Failed: my-stack'))) + + handle.release() + clock.tick(5000) + }) + + it('disposes after all operations complete', function () { + const handle1 = createDeploymentStatusBar('stack1', 'Validation') + const handle2 = createDeploymentStatusBar('stack2', 'Deployment', 'changeset1') + const statusBar = getTestWindow().statusBar + + updateWorkflowStatus(handle1, StackActionPhase.VALIDATION_COMPLETE) + updateWorkflowStatus(handle2, StackActionPhase.DEPLOYMENT_COMPLETE) + + handle1.release() + handle2.release() + + const beforeDispose = statusBar.items.length + clock.tick(5000) + const afterDispose = statusBar.items.filter((item) => !item.disposed).length + + assert.ok(afterDispose < beforeDispose) + }) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/utils/onlineErrorHandler.test.ts b/packages/core/src/test/awsService/cloudformation/utils/onlineErrorHandler.test.ts new file mode 100644 index 00000000000..2b7b93f025d --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/utils/onlineErrorHandler.test.ts @@ -0,0 +1,222 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { ResponseError } from 'vscode-languageclient' +import { handleLspError } from '../../../../awsService/cloudformation/utils/onlineErrorHandler' +import { getTestWindow } from '../../../shared/vscode/window' + +describe('handleLspError', function () { + let sandbox: sinon.SinonSandbox + let executeCommandStub: sinon.SinonStub + + beforeEach(function () { + sandbox = sinon.createSandbox() + executeCommandStub = sandbox.stub(vscode.commands, 'executeCommand') + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('Non-LSP Errors', function () { + it('should handle regular Error without context', async function () { + const error = new Error('Something went wrong') + await handleLspError(error) + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'Something went wrong') + }) + + it('should handle regular Error with context', async function () { + const error = new Error('Something went wrong') + await handleLspError(error, 'Error deploying template') + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'Error deploying template: Something went wrong') + }) + + it('should handle string error', async function () { + await handleLspError('String error') + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'String error') + }) + + it('should handle unknown error type', async function () { + await handleLspError({ random: 'object' }) + assert.strictEqual(getTestWindow().shownMessages.length, 1) + }) + }) + + describe('ExpiredCredentials Error (-32003)', function () { + it('should show Re-authenticate button when requiresReauth is true', async function () { + const error = new ResponseError(-32_003, 'AWS credentials are invalid or expired', { + requiresReauth: true, + retryable: false, + }) + getTestWindow().onDidShowMessage((message) => { + message.selectItem('Re-authenticate') + }) + await handleLspError(error) + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'AWS credentials are invalid or expired') + assert.ok(executeCommandStub.calledWith('aws.toolkit.login')) + }) + + it('should not trigger login if user dismisses', async function () { + const error = new ResponseError(-32_003, 'AWS credentials are invalid or expired', { + requiresReauth: true, + retryable: false, + }) + getTestWindow().onDidShowMessage((message) => { + message.dispose() + }) + await handleLspError(error) + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'AWS credentials are invalid or expired') + assert.ok(executeCommandStub.notCalled) + }) + + it('should show message without button when requiresReauth is false', async function () { + const error = new ResponseError(-32_003, 'AWS credentials are invalid or expired', { + requiresReauth: false, + retryable: false, + }) + await handleLspError(error) + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'AWS credentials are invalid or expired') + assert.ok(executeCommandStub.notCalled) + }) + + it('should include context in message', async function () { + const error = new ResponseError(-32_003, 'AWS credentials are invalid or expired', { + requiresReauth: true, + retryable: false, + }) + getTestWindow().onDidShowMessage((message) => { + message.dispose() + }) + await handleLspError(error, 'Error deploying template') + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'Error deploying template: AWS credentials are invalid or expired') + }) + }) + + describe('NoAuthentication Error (-32002)', function () { + it('should show Re-authenticate button when requiresReauth is true', async function () { + const error = new ResponseError(-32_002, 'No AWS credentials configured', { + requiresReauth: true, + retryable: false, + }) + getTestWindow().onDidShowMessage((message) => { + message.selectItem('Re-authenticate') + }) + await handleLspError(error) + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'No AWS credentials configured') + assert.ok(executeCommandStub.calledWith('aws.toolkit.login')) + }) + + it('should show message without button when requiresReauth is false', async function () { + const error = new ResponseError(-32_002, 'No AWS credentials configured', { + requiresReauth: false, + retryable: false, + }) + await handleLspError(error) + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'No AWS credentials configured') + assert.ok(executeCommandStub.notCalled) + }) + }) + + describe('NoInternet Error (-32001)', function () { + it('should show message without retry button', async function () { + const error = new ResponseError(-32_001, 'Network error occurred while contacting AWS', { + retryable: true, + requiresReauth: false, + }) + await handleLspError(error) + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'Network error occurred while contacting AWS') + }) + + it('should include context in message', async function () { + const error = new ResponseError(-32_001, 'Network error occurred while contacting AWS', { + retryable: true, + requiresReauth: false, + }) + await handleLspError(error, 'Error loading stacks') + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'Error loading stacks: Network error occurred while contacting AWS') + }) + }) + + describe('AwsServiceError (-32004)', function () { + it('should show message when retryable is true', async function () { + const error = new ResponseError(-32_004, 'AWS service error: Stack not found', { + retryable: true, + requiresReauth: false, + }) + await handleLspError(error) + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'AWS service error: Stack not found') + }) + + it('should show message when retryable is false', async function () { + const error = new ResponseError(-32_004, 'AWS service error: Access denied', { + retryable: false, + requiresReauth: false, + }) + await handleLspError(error) + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'AWS service error: Access denied') + }) + + it('should include context in message', async function () { + const error = new ResponseError(-32_004, 'AWS service error: Stack not found', { + retryable: false, + requiresReauth: false, + }) + await handleLspError(error, 'Error viewing stack') + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'Error viewing stack: AWS service error: Stack not found') + }) + }) + + describe('Unknown LSP Error Code', function () { + it('should show message for unknown error code', async function () { + const error = new ResponseError(-99_999, 'Unknown error') + await handleLspError(error) + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'Unknown error') + }) + + it('should include context for unknown error code', async function () { + const error = new ResponseError(-99_999, 'Unknown error') + await handleLspError(error, 'Error performing operation') + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'Error performing operation: Unknown error') + }) + }) + + describe('Edge Cases', function () { + it('should handle LSP error without data field', async function () { + const error = new ResponseError(-32_003, 'Credentials expired') + await handleLspError(error) + const message = getTestWindow().getFirstMessage() + assert.strictEqual(message.message, 'Credentials expired') + }) + + it('should handle null error', async function () { + await handleLspError(undefined) + assert.strictEqual(getTestWindow().shownMessages.length, 1) + }) + + it('should handle undefined error', async function () { + await handleLspError(undefined) + assert.strictEqual(getTestWindow().shownMessages.length, 1) + }) + }) +}) diff --git a/packages/core/src/test/awsService/iot/commands/attachCertificate.test.ts b/packages/core/src/test/awsService/iot/commands/attachCertificate.test.ts index f0796e9cc60..840c375a80c 100644 --- a/packages/core/src/test/awsService/iot/commands/attachCertificate.test.ts +++ b/packages/core/src/test/awsService/iot/commands/attachCertificate.test.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode' import * as sinon from 'sinon' -import { Iot } from 'aws-sdk' +import { Certificate } from '@aws-sdk/client-iot' import { attachCertificateCommand, CertGen } from '../../../../awsService/iot/commands/attachCertificate' import { IotThingFolderNode } from '../../../../awsService/iot/explorer/iotThingFolderNode' import { IotThingNode } from '../../../../awsService/iot/explorer/iotThingNode' @@ -19,22 +19,22 @@ import assert from 'assert' describe('attachCertCommand', function () { const thingName = 'iot-thing' let iot: IotClient - let certs: Iot.Certificate[] + let certs: Certificate[] let thingNode: IotThingNode let selection: number = 0 let sandbox: sinon.SinonSandbox let spyExecuteCommand: sinon.SinonSpy - const prompt: (iot: IotClient, certFetch: CertGen) => Promise> = async ( + const prompt: (iot: IotClient, certFetch: CertGen) => Promise> = async ( iot, certFetch ) => { const iterable = certFetch(iot) - const responses: DataQuickPickItem[] = [] + const responses: DataQuickPickItem[] = [] for await (const response of iterable) { responses.push(...response) } - return selection > -1 ? (responses[selection].data as Iot.Certificate) : undefined + return selection > -1 ? (responses[selection].data as Certificate) : undefined } beforeEach(function () { diff --git a/packages/core/src/test/awsService/iot/commands/createCert.test.ts b/packages/core/src/test/awsService/iot/commands/createCert.test.ts index 00f87b4c8a8..1f280629410 100644 --- a/packages/core/src/test/awsService/iot/commands/createCert.test.ts +++ b/packages/core/src/test/awsService/iot/commands/createCert.test.ts @@ -9,7 +9,7 @@ import { createCertificateCommand } from '../../../../awsService/iot/commands/cr import { IotNode } from '../../../../awsService/iot/explorer/iotNodes' import { IotClient } from '../../../../shared/clients/iotClient' import { IotCertsFolderNode } from '../../../../awsService/iot/explorer/iotCertFolderNode' -import { Iot } from 'aws-sdk' +import { CreateKeysAndCertificateResponse } from '@aws-sdk/client-iot' import { getTestWindow } from '../../../shared/vscode/window' import assert from 'assert' @@ -21,7 +21,7 @@ describe('createCertificateCommand', function () { const certificateArn = 'arn' const certificatePem = 'certPem' const keyPair = { PrivateKey: 'private', PublicKey: 'public' } - const certificate: Iot.CreateKeysAndCertificateResponse = { certificateId, certificateArn, certificatePem, keyPair } + const certificate: CreateKeysAndCertificateResponse = { certificateId, certificateArn, certificatePem, keyPair } let iot: IotClient let node: IotCertsFolderNode let saveLocation: vscode.Uri | undefined = vscode.Uri.file('/certificate.txt') diff --git a/packages/core/src/test/awsService/iot/commands/deletePolicy.test.ts b/packages/core/src/test/awsService/iot/commands/deletePolicy.test.ts index fc0c748317f..9812b915f36 100644 --- a/packages/core/src/test/awsService/iot/commands/deletePolicy.test.ts +++ b/packages/core/src/test/awsService/iot/commands/deletePolicy.test.ts @@ -5,7 +5,7 @@ import * as sinon from 'sinon' import * as vscode from 'vscode' -import { Iot } from 'aws-sdk' +import { PolicyVersion } from '@aws-sdk/client-iot' import { deletePolicyCommand } from '../../../../awsService/iot/commands/deletePolicy' import { IotPolicyFolderNode } from '../../../../awsService/iot/explorer/iotPolicyFolderNode' import { IotPolicyWithVersionsNode } from '../../../../awsService/iot/explorer/iotPolicyNode' @@ -42,8 +42,8 @@ describe('deletePolicyCommand', function () { iot.listPolicyTargets = listPolicyStub const policyVersions = ['1'] const listPolicyVersionsStub = sinon.stub().returns( - asyncGenerator( - policyVersions.map((versionId) => { + asyncGenerator( + policyVersions.map((versionId) => { return { versionId: versionId, } @@ -86,8 +86,8 @@ describe('deletePolicyCommand', function () { iot.listPolicyTargets = listPolicyStub const policyVersions = ['1', '2'] const listPolicyVersionsStub = sinon.stub().returns( - asyncGenerator( - policyVersions.map((versionId) => { + asyncGenerator( + policyVersions.map((versionId) => { return { versionId: versionId, } diff --git a/packages/core/src/test/awsService/iot/explorer/iotCertFolderNode.test.ts b/packages/core/src/test/awsService/iot/explorer/iotCertFolderNode.test.ts index 3d0905ad3a7..7247b1df632 100644 --- a/packages/core/src/test/awsService/iot/explorer/iotCertFolderNode.test.ts +++ b/packages/core/src/test/awsService/iot/explorer/iotCertFolderNode.test.ts @@ -7,7 +7,7 @@ import assert from 'assert' import { MoreResultsNode } from '../../../../awsexplorer/moreResultsNode' import { IotNode } from '../../../../awsService/iot/explorer/iotNodes' import { IotCertificate, IotClient } from '../../../../shared/clients/iotClient' -import { Iot } from 'aws-sdk' +import { Certificate } from '@aws-sdk/client-iot' import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' import { IotCertWithPoliciesNode } from '../../../../awsService/iot/explorer/iotCertificateNode' import { IotCertsFolderNode } from '../../../../awsService/iot/explorer/iotCertFolderNode' @@ -21,7 +21,7 @@ describe('IotCertFolderNode', function () { let iot: IotClient let config: TestSettings - const cert: Iot.Certificate = { + const cert: Certificate = { certificateId: 'cert', certificateArn: 'arn', status: 'ACTIVE', diff --git a/packages/core/src/test/awsService/iot/explorer/iotCertificateNode.test.ts b/packages/core/src/test/awsService/iot/explorer/iotCertificateNode.test.ts index 911b234d8f9..3afdcb6181d 100644 --- a/packages/core/src/test/awsService/iot/explorer/iotCertificateNode.test.ts +++ b/packages/core/src/test/awsService/iot/explorer/iotCertificateNode.test.ts @@ -6,7 +6,7 @@ import assert from 'assert' import { MoreResultsNode } from '../../../../awsexplorer/moreResultsNode' import { IotClient, IotPolicy } from '../../../../shared/clients/iotClient' -import { Iot } from 'aws-sdk' +import { Policy } from '@aws-sdk/client-iot' import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' import { IotPolicyCertNode } from '../../../../awsService/iot/explorer/iotPolicyNode' import { IotCertWithPoliciesNode } from '../../../../awsService/iot/explorer/iotCertificateNode' @@ -22,7 +22,7 @@ describe('IotCertificateNode', function () { let config: TestSettings const certArn = 'certArn' const cert = { id: 'cert', arn: certArn, activeStatus: 'ACTIVE', creationDate: new Date(0) } - const policy: Iot.Policy = { policyName: 'policy', policyArn: 'arn' } + const policy: Policy = { policyName: 'policy', policyArn: 'arn' } const expectedPolicy: IotPolicy = { name: 'policy', arn: 'arn' } function assertPolicyNode(node: AWSTreeNodeBase, expectedPolicy: IotPolicy): void { diff --git a/packages/core/src/test/awsService/iot/explorer/iotPolicyFolderNode.test.ts b/packages/core/src/test/awsService/iot/explorer/iotPolicyFolderNode.test.ts index d8da4e97585..18e91a02531 100644 --- a/packages/core/src/test/awsService/iot/explorer/iotPolicyFolderNode.test.ts +++ b/packages/core/src/test/awsService/iot/explorer/iotPolicyFolderNode.test.ts @@ -7,7 +7,7 @@ import assert from 'assert' import { MoreResultsNode } from '../../../../awsexplorer/moreResultsNode' import { IotNode } from '../../../../awsService/iot/explorer/iotNodes' import { IotClient, IotPolicy } from '../../../../shared/clients/iotClient' -import { Iot } from 'aws-sdk' +import { Policy } from '@aws-sdk/client-iot' import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' import { IotPolicyWithVersionsNode } from '../../../../awsService/iot/explorer/iotPolicyNode' import { IotPolicyFolderNode } from '../../../../awsService/iot/explorer/iotPolicyFolderNode' @@ -20,7 +20,7 @@ describe('IotPolicyFolderNode', function () { let iot: IotClient let config: TestSettings - const policy: Iot.Policy = { policyName: 'policy', policyArn: 'arn' } + const policy: Policy = { policyName: 'policy', policyArn: 'arn' } const expectedPolicy: IotPolicy = { name: 'policy', arn: 'arn' } function assertPolicyNode(node: AWSTreeNodeBase, expectedPolicy: IotPolicy): void { diff --git a/packages/core/src/test/awsService/iot/explorer/iotPolicyNode.test.ts b/packages/core/src/test/awsService/iot/explorer/iotPolicyNode.test.ts index 795c0fae396..88e55b929df 100644 --- a/packages/core/src/test/awsService/iot/explorer/iotPolicyNode.test.ts +++ b/packages/core/src/test/awsService/iot/explorer/iotPolicyNode.test.ts @@ -5,7 +5,7 @@ import assert from 'assert' import { IotClient, IotPolicy } from '../../../../shared/clients/iotClient' -import { Iot } from 'aws-sdk' +import { PolicyVersion } from '@aws-sdk/client-iot' import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' import { asyncGenerator } from '../../../../shared/utilities/collectionUtils' import { IotPolicyWithVersionsNode } from '../../../../awsService/iot/explorer/iotPolicyNode' @@ -19,12 +19,12 @@ describe('IotPolicyNode', function () { let config: TestSettings const policyName = 'policy' const expectedPolicy: IotPolicy = { name: policyName, arn: 'arn' } - const policyVersion: Iot.PolicyVersion = { versionId: 'V1', isDefaultVersion: true } + const policyVersion: PolicyVersion = { versionId: 'V1', isDefaultVersion: true } function assertPolicyVersionNode( node: AWSTreeNodeBase, expectedPolicy: IotPolicy, - expectedVersion: Iot.PolicyVersion + expectedVersion: PolicyVersion ): void { assert.ok(node instanceof IotPolicyVersionNode, `Node ${node} should be a Policy Version Node`) assert.deepStrictEqual((node as IotPolicyVersionNode).version, expectedVersion) @@ -39,7 +39,7 @@ describe('IotPolicyNode', function () { describe('getChildren', function () { it('gets children', async function () { const versions = [{ versionId: 'V1', isDefaultVersion: true }] - const stub = sinon.stub().returns(asyncGenerator(versions)) + const stub = sinon.stub().returns(asyncGenerator(versions)) iot.listPolicyVersions = stub const node = new IotPolicyWithVersionsNode( expectedPolicy, diff --git a/packages/core/src/test/awsService/iot/explorer/iotPolicyVersionNode.test.ts b/packages/core/src/test/awsService/iot/explorer/iotPolicyVersionNode.test.ts index 415e4f29fe7..4faf56e743d 100644 --- a/packages/core/src/test/awsService/iot/explorer/iotPolicyVersionNode.test.ts +++ b/packages/core/src/test/awsService/iot/explorer/iotPolicyVersionNode.test.ts @@ -5,7 +5,7 @@ import assert from 'assert' import { IotClient, IotPolicy } from '../../../../shared/clients/iotClient' -import { Iot } from 'aws-sdk' +import { PolicyVersion } from '@aws-sdk/client-iot' import { IotPolicyWithVersionsNode } from '../../../../awsService/iot/explorer/iotPolicyNode' import { IotPolicyVersionNode } from '../../../../awsService/iot/explorer/iotPolicyVersionNode' import { stringOrProp } from '../../../../shared/utilities/tsUtils' @@ -16,8 +16,8 @@ describe('IotPolicyVersionNode', function () { const expectedPolicy: IotPolicy = { name: policyName, arn: 'arn' } const createDate = new Date(Date.UTC(2021, 1, 1)) // Feb 1 UTC = Jan 31 PDT const createDateFormatted = formatLocalized(createDate) - const policyVersion: Iot.PolicyVersion = { versionId: 'V1', isDefaultVersion: true, createDate } - const nonDefaultVersion: Iot.PolicyVersion = { versionId: 'V2', isDefaultVersion: false, createDate } + const policyVersion: PolicyVersion = { versionId: 'V1', isDefaultVersion: true, createDate } + const nonDefaultVersion: PolicyVersion = { versionId: 'V2', isDefaultVersion: false, createDate } it('creates an IoT Policy Version Node for default version', async function () { const node = new IotPolicyVersionNode( diff --git a/packages/core/src/test/awsService/iot/explorer/iotThingFolderNode.test.ts b/packages/core/src/test/awsService/iot/explorer/iotThingFolderNode.test.ts index db25a689fbd..667b39974b7 100644 --- a/packages/core/src/test/awsService/iot/explorer/iotThingFolderNode.test.ts +++ b/packages/core/src/test/awsService/iot/explorer/iotThingFolderNode.test.ts @@ -9,7 +9,7 @@ import { IotNode } from '../../../../awsService/iot/explorer/iotNodes' import { IotThingFolderNode } from '../../../../awsService/iot/explorer/iotThingFolderNode' import { IotThingNode } from '../../../../awsService/iot/explorer/iotThingNode' import { IotClient, IotThing } from '../../../../shared/clients/iotClient' -import { Iot } from 'aws-sdk' +import { ThingAttribute } from '@aws-sdk/client-iot' import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' import { TestSettings } from '../../../utilities/testSettingsConfiguration' import sinon from 'sinon' @@ -20,7 +20,7 @@ describe('IotThingFolderNode', function () { let iot: IotClient let config: TestSettings - const thing: Iot.ThingAttribute = { thingName: 'thing', thingArn: 'arn' } + const thing: ThingAttribute = { thingName: 'thing', thingArn: 'arn' } const expectedThing: IotThing = { name: 'thing', arn: 'arn' } function assertThingNode(node: AWSTreeNodeBase, expectedThing: IotThing): void { diff --git a/packages/core/src/test/awsService/iot/explorer/iotThingNode.test.ts b/packages/core/src/test/awsService/iot/explorer/iotThingNode.test.ts index 52d0d92e060..663860ef968 100644 --- a/packages/core/src/test/awsService/iot/explorer/iotThingNode.test.ts +++ b/packages/core/src/test/awsService/iot/explorer/iotThingNode.test.ts @@ -6,7 +6,7 @@ import assert from 'assert' import { MoreResultsNode } from '../../../../awsexplorer/moreResultsNode' import { IotCertificate, IotClient } from '../../../../shared/clients/iotClient' -import { Iot } from 'aws-sdk' +import { Certificate } from '@aws-sdk/client-iot' import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' import { IotThingCertNode } from '../../../../awsService/iot/explorer/iotCertificateNode' import { IotThingNode } from '../../../../awsService/iot/explorer/iotThingNode' @@ -22,7 +22,7 @@ describe('IotThingNode', function () { let config: TestSettings const thingName = 'thing' const thing = { name: thingName, arn: 'thingArn' } - const cert: Iot.Certificate = { + const cert: Certificate = { certificateId: 'cert', certificateArn: 'arn', status: 'ACTIVE', diff --git a/packages/core/src/test/awsService/redshift/explorer/redshiftDatabaseNode.test.ts b/packages/core/src/test/awsService/redshift/explorer/redshiftDatabaseNode.test.ts index 07f81a0dfea..f291b926c0c 100644 --- a/packages/core/src/test/awsService/redshift/explorer/redshiftDatabaseNode.test.ts +++ b/packages/core/src/test/awsService/redshift/explorer/redshiftDatabaseNode.test.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import sinon = require('sinon') +import { mockClient } from 'aws-sdk-client-mock' import { RedshiftDatabaseNode } from '../../../../awsService/redshift/explorer/redshiftDatabaseNode' -import { RedshiftData } from 'aws-sdk' +import { RedshiftDataClient, ListSchemasCommand } from '@aws-sdk/client-redshift-data' import { DefaultRedshiftClient } from '../../../../shared/clients/redshiftClient' import { ConnectionParams, ConnectionType, RedshiftWarehouseType } from '../../../../awsService/redshift/models/models' import assert = require('assert') @@ -14,44 +14,37 @@ import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBa import { MoreResultsNode } from '../../../../awsexplorer/moreResultsNode' describe('RedshiftDatabaseNode', function () { - const sandbox = sinon.createSandbox() - const mockRedshiftData = {} - const redshiftClient = new DefaultRedshiftClient('us-east-1', async () => mockRedshiftData, undefined, undefined) + const mockRedshiftData = mockClient(RedshiftDataClient) + const redshiftClient = new DefaultRedshiftClient('us-east-1', () => mockRedshiftData as any, undefined, undefined) const connectionParams = new ConnectionParams( ConnectionType.TempCreds, 'testDb1', 'warehouseId', RedshiftWarehouseType.PROVISIONED ) - let listSchemasStub: sinon.SinonStub describe('getChildren', function () { - beforeEach(function () { - listSchemasStub = sandbox.stub() - mockRedshiftData.listSchemas = listSchemasStub - }) - afterEach(function () { - sandbox.reset() + mockRedshiftData.reset() }) it('loads schemas successfully', async () => { const node = new RedshiftDatabaseNode('testDB1', redshiftClient, connectionParams) - listSchemasStub.returns({ promise: () => Promise.resolve({ Schemas: ['schema1'] }) }) + mockRedshiftData.on(ListSchemasCommand).resolves({ Schemas: ['schema1'] }) const childNodes = await node.getChildren() verifyChildNodes(childNodes, false) }) it('loads schemas and shows load more node when there are more schemas', async () => { const node = new RedshiftDatabaseNode('testDB1', redshiftClient, connectionParams) - listSchemasStub.returns({ promise: () => Promise.resolve({ Schemas: ['schema1'], NextToken: 'next' }) }) + mockRedshiftData.on(ListSchemasCommand).resolves({ Schemas: ['schema1'], NextToken: 'next' }) const childNodes = await node.getChildren() verifyChildNodes(childNodes, true) }) it('shows error node when listSchema fails', async () => { const node = new RedshiftDatabaseNode('testDB1', redshiftClient, connectionParams) - listSchemasStub.returns({ promise: () => Promise.reject('Failed') }) + mockRedshiftData.on(ListSchemasCommand).rejects('Failed') const childNodes = await node.getChildren() assert.strictEqual(childNodes.length, 1) assert.strictEqual(childNodes[0].contextValue, 'awsErrorNode') diff --git a/packages/core/src/test/awsService/redshift/explorer/redshiftNode.test.ts b/packages/core/src/test/awsService/redshift/explorer/redshiftNode.test.ts index af9c9ffd5ce..26df4f5abf7 100644 --- a/packages/core/src/test/awsService/redshift/explorer/redshiftNode.test.ts +++ b/packages/core/src/test/awsService/redshift/explorer/redshiftNode.test.ts @@ -3,29 +3,27 @@ * SPDX-License-Identifier: Apache-2.0 */ // eslint-disable-next-line header/header -import sinon = require('sinon') +import { mockClient, AwsClientStub } from 'aws-sdk-client-mock' import { RedshiftNode } from '../../../../awsService/redshift/explorer/redshiftNode' import { DefaultRedshiftClient } from '../../../../shared/clients/redshiftClient' -import { AWSError, Redshift, RedshiftServerless, Request } from 'aws-sdk' import assert = require('assert') import { RedshiftWarehouseNode } from '../../../../awsService/redshift/explorer/redshiftWarehouseNode' -import { ClusterList, ClustersMessage } from 'aws-sdk/clients/redshift' -import { ListWorkgroupsResponse, WorkgroupList } from 'aws-sdk/clients/redshiftserverless' +import { Cluster, ClustersMessage, RedshiftClient, DescribeClustersCommand } from '@aws-sdk/client-redshift' +import { + ListWorkgroupsResponse, + Workgroup, + RedshiftServerlessClient, + ListWorkgroupsCommand, +} from '@aws-sdk/client-redshift-serverless' import { RedshiftWarehouseType } from '../../../../awsService/redshift/models/models' import { MoreResultsNode } from '../../../../awsexplorer/moreResultsNode' import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' -function success(output?: T): Request { - return { - promise: () => Promise.resolve(output), - } as Request -} - function getExpectedProvisionedResponse(withNextToken: boolean): ClustersMessage { const response = { Clusters: [ { ClusterNamespaceArn: 'testArn', ClusterIdentifier: 'testId', ClusterAvailabilityStatus: 'available' }, - ] as ClusterList, + ] as Cluster[], } as ClustersMessage if (withNextToken) { response.Marker = 'next' @@ -35,7 +33,7 @@ function getExpectedProvisionedResponse(withNextToken: boolean): ClustersMessage function getExpectedServerlessResponse(withNextToken: boolean): ListWorkgroupsResponse { const response = { - workgroups: [{ workgroupArn: 'testArn', workgroupName: 'testWorkgroup', status: 'available' }] as WorkgroupList, + workgroups: [{ workgroupArn: 'testArn', workgroupName: 'testWorkgroup', status: 'AVAILABLE' }] as Workgroup[], } as ListWorkgroupsResponse if (withNextToken) { response.nextToken = 'next' @@ -70,79 +68,59 @@ describe('redshiftNode', function () { describe('getChildren', function () { let node: RedshiftNode let redshiftClient: DefaultRedshiftClient - let mockRedshift: Redshift - let mockRedshiftServerless: RedshiftServerless - const sandbox: sinon.SinonSandbox = sinon.createSandbox() - const describeClustersStub = sandbox.stub() - const listWorkgroupsStub = sandbox.stub() - - function verifyStubCallCounts(describeClustersStubCallCount: number, listWorkgroupsStubCallCount: number) { - assert.strictEqual( - describeClustersStub.callCount, - describeClustersStubCallCount, - 'DescribeClustersStub call count mismatch' - ) - assert.strictEqual( - listWorkgroupsStub.callCount, - listWorkgroupsStubCallCount, - 'ListWorkgroupsStub call count mismatch' - ) - } + let mockRedshift: AwsClientStub + let mockRedshiftServerless: AwsClientStub beforeEach(function () { - mockRedshift = {} - mockRedshiftServerless = {} + mockRedshift = mockClient(RedshiftClient) + mockRedshiftServerless = mockClient(RedshiftServerlessClient) redshiftClient = new DefaultRedshiftClient( 'us-east-1', undefined, - async (r) => Promise.resolve(mockRedshift), - async (r) => Promise.resolve(mockRedshiftServerless) + // @ts-expect-error + () => mockRedshift, + () => mockRedshiftServerless ) - mockRedshift.describeClusters = describeClustersStub - mockRedshiftServerless.listWorkgroups = listWorkgroupsStub node = new RedshiftNode(redshiftClient) }) afterEach(function () { - sandbox.reset() + mockRedshift.reset() + mockRedshiftServerless.reset() }) it('gets both provisioned and serverless warehouses when no results have been loaded', async () => { - describeClustersStub.returns(success(getExpectedProvisionedResponse(false))) - listWorkgroupsStub.returns(success(getExpectedServerlessResponse(false))) + mockRedshift.on(DescribeClustersCommand).resolves(getExpectedProvisionedResponse(false)) + mockRedshiftServerless.on(ListWorkgroupsCommand).resolves(getExpectedServerlessResponse(false)) const childNodes = await node.getChildren() verifyChildNodeCounts(childNodes, 1, 1, 0) - verifyStubCallCounts(1, 1) }) it('gets both provisioned and serverless warehouses if results have been loaded but there are more results', async () => { - describeClustersStub.returns(success(getExpectedProvisionedResponse(true))) - listWorkgroupsStub.returns(success(getExpectedServerlessResponse(true))) + mockRedshift.on(DescribeClustersCommand).resolves(getExpectedProvisionedResponse(true)) + mockRedshiftServerless.on(ListWorkgroupsCommand).resolves(getExpectedServerlessResponse(true)) const childNodes = await node.getChildren() verifyChildNodeCounts(childNodes, 1, 1, 1) - verifyStubCallCounts(1, 1) }) it('gets only provisioned warehouses if results have been loaded and there are only more provisioned warehouses', async () => { - describeClustersStub.returns(success(getExpectedProvisionedResponse(true))) - listWorkgroupsStub.returns(success(getExpectedServerlessResponse(false))) + mockRedshift.on(DescribeClustersCommand).resolves(getExpectedProvisionedResponse(true)) + mockRedshiftServerless.on(ListWorkgroupsCommand).resolves(getExpectedServerlessResponse(false)) const childNodes = await node.getChildren() verifyChildNodeCounts(childNodes, 1, 1, 1) await node.loadMoreChildren() const newChildNodes = await node.getChildren() verifyChildNodeCounts(newChildNodes, 2, 1, 1) - verifyStubCallCounts(2, 1) }) it('gets only serverless warehouses if results have been loaded and there are only more serverless warehouses', async () => { - describeClustersStub.returns(success(getExpectedProvisionedResponse(false))) - listWorkgroupsStub.returns(success(getExpectedServerlessResponse(true))) + mockRedshift.on(DescribeClustersCommand).resolves(getExpectedProvisionedResponse(false)) + mockRedshiftServerless.on(ListWorkgroupsCommand).resolves(getExpectedServerlessResponse(true)) const childNodes = await node.getChildren() verifyChildNodeCounts(childNodes, 1, 1, 1) await node.loadMoreChildren() const newChildNodes = await node.getChildren() verifyChildNodeCounts(newChildNodes, 1, 2, 1) - verifyStubCallCounts(1, 2) }) }) }) diff --git a/packages/core/src/test/awsService/redshift/explorer/redshiftSchemaNode.test.ts b/packages/core/src/test/awsService/redshift/explorer/redshiftSchemaNode.test.ts index a3e4a17f4aa..68c2d792cfa 100644 --- a/packages/core/src/test/awsService/redshift/explorer/redshiftSchemaNode.test.ts +++ b/packages/core/src/test/awsService/redshift/explorer/redshiftSchemaNode.test.ts @@ -3,22 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as sinon from 'sinon' +import { mockClient, AwsClientStub } from 'aws-sdk-client-mock' import * as assert from 'assert' import { DefaultRedshiftClient } from '../../../../shared/clients/redshiftClient' -import { RedshiftData } from 'aws-sdk' +import { RedshiftDataClient, ListTablesCommand, ListTablesResponse } from '@aws-sdk/client-redshift-data' import { RedshiftSchemaNode } from '../../../../awsService/redshift/explorer/redshiftSchemaNode' import { ConnectionParams, ConnectionType, RedshiftWarehouseType } from '../../../../awsService/redshift/models/models' import { RedshiftTableNode } from '../../../../awsService/redshift/explorer/redshiftTableNode' -import { ListTablesResponse } from 'aws-sdk/clients/redshiftdata' import { MoreResultsNode } from '../../../../awsexplorer/moreResultsNode' describe('RedshiftSchemaNode', function () { - const sandbox = sinon.createSandbox() - const mockRedshiftData: RedshiftData = {} + const mockRedshiftData: AwsClientStub = mockClient(RedshiftDataClient) const redshiftClient: DefaultRedshiftClient = new DefaultRedshiftClient( 'us-east-1', - async () => mockRedshiftData, + // @ts-expect-error + () => mockRedshiftData, undefined, undefined ) @@ -28,23 +27,16 @@ describe('RedshiftSchemaNode', function () { 'warehouseId', RedshiftWarehouseType.PROVISIONED ) - let listTablesStub: sinon.SinonStub describe('getChildren', function () { - beforeEach(function () { - listTablesStub = sandbox.stub() - mockRedshiftData.listTables = listTablesStub - }) - afterEach(function () { - sandbox.reset() + mockRedshiftData.reset() }) it('gets table nodes and filters out tables with pkey', async () => { - listTablesStub.returns({ - promise: () => - Promise.resolve({ Tables: [{ name: 'test' }, { name: 'test_pkey' }] } as ListTablesResponse), - }) + mockRedshiftData + .on(ListTablesCommand) + .resolves({ Tables: [{ name: 'test' }, { name: 'test_pkey' }] } as ListTablesResponse) const node = new RedshiftSchemaNode('testSchema', redshiftClient, connectionParams) const childNodes = await node.getChildren() assert.strictEqual(childNodes.length, 1) @@ -52,13 +44,10 @@ describe('RedshiftSchemaNode', function () { }) it('gets table nodes & adds load more node if there are more nodes to be loaded', async () => { - listTablesStub.returns({ - promise: () => - Promise.resolve({ - Tables: [{ name: 'test' }, { name: 'test_pkey' }], - NextToken: 'next', - } as ListTablesResponse), - }) + mockRedshiftData.on(ListTablesCommand).resolves({ + Tables: [{ name: 'test' }, { name: 'test_pkey' }], + NextToken: 'next', + } as ListTablesResponse) const node = new RedshiftSchemaNode('testSchema', redshiftClient, connectionParams) const childNodes = await node.getChildren() assert.strictEqual(childNodes.length, 2) @@ -67,7 +56,7 @@ describe('RedshiftSchemaNode', function () { }) it('shows error node when list table API errors out', async () => { - listTablesStub.returns({ promise: () => Promise.reject('failed') }) + mockRedshiftData.on(ListTablesCommand).rejects('failed') const node = new RedshiftSchemaNode('testSchema', redshiftClient, connectionParams) const childNodes = await node.getChildren() assert.strictEqual(childNodes.length, 1) diff --git a/packages/core/src/test/awsService/redshift/explorer/redshiftWarehouseNode.test.ts b/packages/core/src/test/awsService/redshift/explorer/redshiftWarehouseNode.test.ts index d06128b2585..b82e9c972f9 100644 --- a/packages/core/src/test/awsService/redshift/explorer/redshiftWarehouseNode.test.ts +++ b/packages/core/src/test/awsService/redshift/explorer/redshiftWarehouseNode.test.ts @@ -5,7 +5,7 @@ import sinon = require('sinon') import { DefaultRedshiftClient } from '../../../../shared/clients/redshiftClient' -import { ListDatabasesResponse } from 'aws-sdk/clients/redshiftdata' +import { ListDatabasesResponse, RedshiftDataClient, ListDatabasesCommand } from '@aws-sdk/client-redshift-data' import { ConnectionParams, ConnectionType, RedshiftWarehouseType } from '../../../../awsService/redshift/models/models' import { RedshiftWarehouseNode, @@ -17,9 +17,9 @@ import * as assert from 'assert' import { RedshiftDatabaseNode } from '../../../../awsService/redshift/explorer/redshiftDatabaseNode' import { AWSCommandTreeNode } from '../../../../shared/treeview/nodes/awsCommandTreeNode' import { RedshiftNodeConnectionWizard } from '../../../../awsService/redshift/wizards/connectionWizard' -import RedshiftData = require('aws-sdk/clients/redshiftdata') import { MoreResultsNode } from '../../../../awsexplorer/moreResultsNode' import { AWSTreeNodeBase } from '../../../../shared/treeview/nodes/awsTreeNodeBase' +import { mockClient, AwsClientStub } from 'aws-sdk-client-mock' function verifyChildNodes(childNodes: AWSTreeNodeBase[], databaseNodeCount: number, shouldHaveLoadMore: boolean) { assert.strictEqual(childNodes.length, databaseNodeCount + (shouldHaveLoadMore ? 1 : 0) + 1) @@ -41,7 +41,6 @@ function verifyRetryNode(childNodes: AWSTreeNodeBase[]) { describe('redshiftWarehouseNode', function () { describe('getChildren', function () { - const sandbox = sinon.createSandbox() const expectedResponse = { Databases: ['testDb1'] } as ListDatabasesResponse const expectedResponseWithNextToken = { Databases: ['testDb1'], NextToken: 'next' } as ListDatabasesResponse const connectionParams = new ConnectionParams( @@ -51,31 +50,32 @@ describe('redshiftWarehouseNode', function () { RedshiftWarehouseType.PROVISIONED ) const resourceNode = { arn: 'testARN', name: 'warehouseId' } as AWSResourceNode - const mockRedshiftData = {} - const redshiftClient = new DefaultRedshiftClient( - 'us-east-1', - async (r) => Promise.resolve(mockRedshiftData), - undefined, - undefined - ) - const redshiftNode = new RedshiftNode(redshiftClient) - let listDatabasesStub: sinon.SinonStub + let mockRedshiftData: AwsClientStub + let redshiftClient: DefaultRedshiftClient + let redshiftNode: RedshiftNode let warehouseNode: RedshiftWarehouseNode let connectionWizardStub: sinon.SinonStub beforeEach(function () { - listDatabasesStub = sandbox.stub() - mockRedshiftData.listDatabases = listDatabasesStub + mockRedshiftData = mockClient(RedshiftDataClient) + redshiftClient = new DefaultRedshiftClient( + 'us-east-1', + // @ts-expect-error + () => mockRedshiftData, + undefined, + undefined + ) + redshiftNode = new RedshiftNode(redshiftClient) }) afterEach(function () { - sandbox.reset() + mockRedshiftData.reset() connectionWizardStub.restore() }) it('gets databases for a warehouse and adds a start button', async () => { connectionWizardStub = sinon.stub(RedshiftNodeConnectionWizard.prototype, 'run').resolves(connectionParams) warehouseNode = new RedshiftWarehouseNode(redshiftNode, resourceNode, RedshiftWarehouseType.PROVISIONED) - listDatabasesStub.returns({ promise: () => Promise.resolve(expectedResponse) }) + mockRedshiftData.on(ListDatabasesCommand).resolves(expectedResponse) const childNodes = await warehouseNode.getChildren() @@ -85,7 +85,7 @@ describe('redshiftWarehouseNode', function () { it('gets databases for a warehouse, adds a start button and a load more button if there are more results', async () => { connectionWizardStub = sinon.stub(RedshiftNodeConnectionWizard.prototype, 'run').resolves(connectionParams) warehouseNode = new RedshiftWarehouseNode(redshiftNode, resourceNode, RedshiftWarehouseType.PROVISIONED) - listDatabasesStub.returns({ promise: () => Promise.resolve(expectedResponseWithNextToken) }) + mockRedshiftData.on(ListDatabasesCommand).resolves(expectedResponseWithNextToken) const childNodes = await warehouseNode.getChildren() @@ -102,7 +102,7 @@ describe('redshiftWarehouseNode', function () { it('shows a node with retry if there is error fetching databases', async () => { connectionWizardStub = sinon.stub(RedshiftNodeConnectionWizard.prototype, 'run').resolves(connectionParams) warehouseNode = new RedshiftWarehouseNode(redshiftNode, resourceNode, RedshiftWarehouseType.PROVISIONED) - listDatabasesStub.returns({ promise: () => Promise.reject('Failed') }) + mockRedshiftData.on(ListDatabasesCommand).rejects('Failed') const childNodes = await warehouseNode.getChildren() verifyRetryNode(childNodes) }) diff --git a/packages/core/src/test/awsService/redshift/notebook/redshiftNotebookController.test.ts b/packages/core/src/test/awsService/redshift/notebook/redshiftNotebookController.test.ts index 982ce2a6c9c..8b9bfd5b546 100644 --- a/packages/core/src/test/awsService/redshift/notebook/redshiftNotebookController.test.ts +++ b/packages/core/src/test/awsService/redshift/notebook/redshiftNotebookController.test.ts @@ -5,19 +5,19 @@ import * as vscode from 'vscode' import { RedshiftNotebookController } from '../../../../awsService/redshift/notebook/redshiftNotebookController' -import sinon = require('sinon') +import { mockClient, AwsClientStub } from 'aws-sdk-client-mock' import assert = require('assert') import { DefaultRedshiftClient } from '../../../../shared/clients/redshiftClient' -import { RedshiftData } from 'aws-sdk' +import { RedshiftDataClient } from '@aws-sdk/client-redshift-data' +import sinon = require('sinon') describe('RedshiftNotebookController', () => { - const mockRedshiftData = {} - const redshiftClient = new DefaultRedshiftClient('us-east-1', async () => mockRedshiftData, undefined, undefined) + const mockRedshiftData: AwsClientStub = mockClient(RedshiftDataClient) + // @ts-expect-error + const redshiftClient = new DefaultRedshiftClient('us-east-1', () => mockRedshiftData, undefined, undefined) let notebookController: any let createNotebookControllerStub: any - let executeQueryStub: sinon.SinonStub beforeEach(() => { - redshiftClient.executeQuery = executeQueryStub createNotebookControllerStub = sinon.stub(vscode.notebooks, 'createNotebookController') const controllerInstanceValue = { supportedLanguages: ['sql'], @@ -29,6 +29,7 @@ describe('RedshiftNotebookController', () => { notebookController = new RedshiftNotebookController(redshiftClient) }) afterEach(() => { + mockRedshiftData.reset() sinon.restore() }) it('validating parameters of a notebook controller instance', () => { diff --git a/packages/core/src/test/awsService/sagemaker/commands.test.ts b/packages/core/src/test/awsService/sagemaker/commands.test.ts new file mode 100644 index 00000000000..8b7a445b1b4 --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/commands.test.ts @@ -0,0 +1,403 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as sinon from 'sinon' +import assert from 'assert' +import { SagemakerClient } from '../../../shared/clients/sagemaker' +import { getTestWindow } from '../../shared/vscode/window' +import { + RemoteAccessRequiredMessage, + InstanceTypeInsufficientMemoryMessage, +} from '../../../awsService/sagemaker/constants' + +// Import types only, actual functions will be dynamically imported +import type { openRemoteConnect as openRemoteConnectStatic } from '../../../awsService/sagemaker/commands' + +describe('SageMaker Commands', () => { + let sandbox: sinon.SinonSandbox + let mockClient: any + let mockNode: any + + beforeEach(() => { + sandbox = sinon.createSandbox() + mockClient = sandbox.createStubInstance(SagemakerClient) + mockNode = { + regionCode: 'us-east-1', + spaceApp: { + DomainId: 'domain-123', + SpaceName: 'test-space', + }, + } + }) + + afterEach(() => { + sandbox.restore() + getTestWindow().dispose() + + for (const key of Object.keys(require.cache)) { + if (key.includes('awsService/sagemaker/commands')) { + delete require.cache[key] + } + } + }) + + describe('openRemoteConnect handler integration tests', () => { + let mockTryRefreshNode: sinon.SinonStub + let mockTryRemoteConnection: sinon.SinonStub + let mockIsRemoteWorkspace: sinon.SinonStub + let openRemoteConnect: typeof openRemoteConnectStatic + + beforeEach(() => { + mockNode = { + regionCode: 'us-east-1', + spaceApp: { + DomainId: 'domain-123', + SpaceName: 'test-space', + App: { + AppType: 'JupyterLab', + AppName: 'default', + }, + SpaceSettingsSummary: { + AppType: 'JupyterLab', + RemoteAccess: 'DISABLED', + }, + }, + getStatus: sandbox.stub().returns('Running'), + } + + // Mock helper functions + mockTryRefreshNode = sandbox.stub().resolves() + mockTryRemoteConnection = sandbox.stub().resolves() + mockIsRemoteWorkspace = sandbox.stub().returns(false) + + sandbox.replace( + require('../../../awsService/sagemaker/explorer/sagemakerSpaceNode'), + 'tryRefreshNode', + mockTryRefreshNode + ) + sandbox.replace( + require('../../../awsService/sagemaker/model'), + 'tryRemoteConnection', + mockTryRemoteConnection + ) + sandbox.replace(require('../../../shared/vscode/env'), 'isRemoteWorkspace', mockIsRemoteWorkspace) + + const freshModule = require('../../../awsService/sagemaker/commands') + openRemoteConnect = freshModule.openRemoteConnect + }) + + describe('handleRunningSpaceWithDisabledAccess', () => { + beforeEach(() => { + mockNode.getStatus.returns('Running') + mockNode.spaceApp.SpaceSettingsSummary.RemoteAccess = 'DISABLED' + }) + + /** + * Test 1: Shows confirmation dialog mentioning "remote access" when instance type is sufficient + * + * Scenario: User tries to connect to a running space that doesn't have remote access enabled, + * but the instance type (ml.t3.large) has sufficient memory for remote access. + * + * Expected behavior: + * - System checks instance type via describeSpace + * - Shows confirmation dialog mentioning only "remote access" (no instance upgrade needed) + * - User confirms, then space is restarted with remote access enabled + * - Connection is established + */ + it('shows confirmation dialog with remote access message when no upgrade needed', async () => { + mockClient.describeSpace.resolves({ + $metadata: {}, + SpaceSettings: { + AppType: 'JupyterLab', + JupyterLabAppSettings: { + DefaultResourceSpec: { + InstanceType: 'ml.t3.large', // Sufficient memory + }, + }, + }, + }) + mockClient.deleteApp.resolves() + mockClient.startSpace.resolves() + mockClient.waitForAppInService.resolves() + + // Setup test window to handle confirmation dialog + getTestWindow().onDidShowMessage((message) => { + if (message.message.includes(RemoteAccessRequiredMessage)) { + message.selectItem('Restart Space and Connect') + } + }) + + await openRemoteConnect(mockNode, {} as any, mockClient) + + // Verify describeSpace was called to check instance type + assert(mockClient.describeSpace.calledOnce) + assert( + mockClient.describeSpace.calledWith({ + DomainId: 'domain-123', + SpaceName: 'test-space', + }) + ) + + // Verify confirmation dialog was shown + const messages = getTestWindow().shownMessages + assert(messages.length > 0) + const confirmMessage = messages.find((m) => m.message.includes('remote access')) + assert(confirmMessage, 'Should show remote access message') + assert(!confirmMessage.message.includes('ml.t3'), 'Should not mention instance type upgrade') + }) + + /** + * Test 2: Shows confirmation dialog mentioning instance upgrade when needed + * + * Scenario: User tries to connect to a running space with an instance type (ml.t3.medium) + * that has insufficient memory for remote access. + * + * Expected behavior: + * - System checks instance type via describeSpace + * - Detects ml.t3.medium is insufficient (needs upgrade to ml.t3.large) + * - Dialog includes current type (ml.t3.medium) and target type (ml.t3.large) + * - User confirms, then space is restarted with upgraded instance and remote access + */ + it('shows confirmation dialog with instance upgrade message when upgrade needed', async () => { + mockClient.describeSpace.resolves({ + $metadata: {}, + SpaceSettings: { + AppType: 'JupyterLab', + JupyterLabAppSettings: { + DefaultResourceSpec: { + InstanceType: 'ml.t3.medium', // Insufficient memory + }, + }, + }, + }) + mockClient.deleteApp.resolves() + mockClient.startSpace.resolves() + mockClient.waitForAppInService.resolves() + + // Setup test window to handle confirmation dialog + getTestWindow().onDidShowMessage((message) => { + if ( + message.message.includes( + InstanceTypeInsufficientMemoryMessage('test-space', 'ml.t3.medium', 'ml.t3.large') + ) + ) { + message.selectItem('Restart Space and Connect') + } + }) + + await openRemoteConnect(mockNode, {} as any, mockClient) + + // Verify describeSpace was called to check instance type + assert(mockClient.describeSpace.calledOnce) + + // Verify confirmation dialog includes instance type upgrade info + const messages = getTestWindow().shownMessages + const expectedMessage = InstanceTypeInsufficientMemoryMessage( + 'test-space', + 'ml.t3.medium', + 'ml.t3.large' + ) + const confirmMessage = messages.find((m) => m.message.includes(expectedMessage)) + assert(confirmMessage, 'Should show instance upgrade message') + }) + + /** + * Test 3: Verifies the full workflow when user confirms + * + * Scenario: User confirms the restart dialog for a running space with disabled remote access. + * + * Expected behavior (in order): + * 1. tryRefreshNode() - Refresh node state before starting + * 2. describeSpace() - Check instance type requirements + * 3. Show confirmation dialog + * 4. User confirms + * 5. deleteApp() - Stop the running space + * 6. startSpace() - Restart with remote access enabled (3rd param = true) + * 7. tryRefreshNode() - Refresh node state after restart + * 8. waitForAppInService() - Wait for space to be ready + * 9. tryRemoteConnection() - Establish the remote connection + */ + it('performs space restart and connection when user confirms', async () => { + mockClient.describeSpace.resolves({ + $metadata: {}, + SpaceSettings: { + AppType: 'JupyterLab', + JupyterLabAppSettings: { + DefaultResourceSpec: { + InstanceType: 'ml.t3.large', + }, + }, + }, + }) + mockClient.deleteApp.resolves() + mockClient.startSpace.resolves() + mockClient.waitForAppInService.resolves() + + // Setup test window to confirm + getTestWindow().onDidShowMessage((message) => { + if (message.items.some((item) => item.title === 'Restart Space and Connect')) { + message.selectItem('Restart Space and Connect') + } + }) + + await openRemoteConnect(mockNode, {} as any, mockClient) + + // Verify tryRefreshNode was called at the start of openRemoteConnect + assert(mockTryRefreshNode.calledBefore(mockClient.deleteApp)) + + // Verify space operations were performed in correct order + assert(mockClient.deleteApp.calledOnce) + assert( + mockClient.deleteApp.calledWith({ + DomainId: 'domain-123', + SpaceName: 'test-space', + AppType: 'JupyterLab', + AppName: 'default', + }) + ) + assert(mockClient.startSpace.calledOnce) + assert(mockClient.startSpace.calledWith('test-space', 'domain-123', true)) // Remote access enabled + + // Verify tryRefreshNode was called after startSpace + assert(mockTryRefreshNode.calledAfter(mockClient.startSpace)) + + assert(mockClient.waitForAppInService.calledOnce) + assert(mockClient.waitForAppInService.calledWith('domain-123', 'test-space', 'JupyterLab')) + assert(mockTryRemoteConnection.calledOnce) + }) + + /** + * Test 4: Verifies nothing happens when user cancels + * + * Scenario: User is shown the confirmation dialog but clicks "Cancel" instead of confirming. + * + * Expected behavior: + * - tryRefreshNode() is called (happens before showing dialog) + * - describeSpace() is called (to check instance type) + * - Confirmation dialog is shown + * - User cancels + * - NO space operations are performed (no deleteApp, startSpace, or connection attempts) + */ + it('does not perform operations when user cancels', async () => { + mockClient.describeSpace.resolves({ + $metadata: {}, + SpaceSettings: { + AppType: 'JupyterLab', + JupyterLabAppSettings: { + DefaultResourceSpec: { + InstanceType: 'ml.t3.large', + }, + }, + }, + }) + + // Setup test window to cancel + getTestWindow().onDidShowMessage((message) => { + message.selectItem('Cancel') + }) + + await openRemoteConnect(mockNode, {} as any, mockClient) + + // Verify tryRefreshNode was called (happens before confirmation) + assert(mockTryRefreshNode.calledOnce) + // Verify no space operations were performed after cancellation + assert(mockClient.deleteApp.notCalled) + assert(mockClient.startSpace.notCalled) + assert(mockTryRemoteConnection.notCalled) + }) + }) + + describe('handleStoppedSpace', () => { + beforeEach(() => { + mockNode.getStatus.returns('Stopped') + }) + + /** + * Test: Starts space and connects without showing confirmation dialog + * + * Scenario: User tries to connect to a stopped space. + * + * Expected behavior: + * - NO confirmation dialog is shown + * - tryRefreshNode() is called at the start + * - startSpace() is called WITHOUT remote access flag (2 params only) + * - tryRefreshNode() is called again after starting + * - waitForAppInService() waits for space to be ready + * - tryRemoteConnection() establishes the connection + * + * Key difference from running space: No confirmation needed because starting + * a stopped space is non-destructive + */ + it('starts space and connects without confirmation', async () => { + mockClient.startSpace.resolves() + mockClient.waitForAppInService.resolves() + + await openRemoteConnect(mockNode, {} as any, mockClient) + + // Verify no confirmation dialog shown for stopped space + const confirmMessages = getTestWindow().shownMessages.filter((m) => + m.message.includes('Restart Space and Connect') + ) + assert.strictEqual(confirmMessages.length, 0, 'Should not show confirmation for stopped space') + + // Verify tryRefreshNode was called at start of openRemoteConnect + assert(mockTryRefreshNode.calledBefore(mockClient.startSpace)) + + // Verify space operations - startSpace is called before withProgress + assert(mockClient.startSpace.calledOnce) + assert(mockClient.startSpace.calledWith('test-space', 'domain-123')) // No remote access flag + + // Verify tryRefreshNode was called after startSpace (before progress) + assert(mockTryRefreshNode.calledAfter(mockClient.startSpace)) + assert.strictEqual(mockTryRefreshNode.callCount, 2) // Once at start, once after startSpace + + // Verify operations inside progress callback + assert(mockClient.waitForAppInService.calledOnce) + assert(mockClient.waitForAppInService.calledWith('domain-123', 'test-space', 'JupyterLab')) + assert(mockTryRemoteConnection.calledOnce) + }) + }) + + describe('handleRunningSpaceWithEnabledAccess', () => { + beforeEach(() => { + mockNode.getStatus.returns('Running') + mockNode.spaceApp.SpaceSettingsSummary.RemoteAccess = 'ENABLED' + }) + + /** + * Test: Connects directly without any space operations + * + * Scenario: User tries to connect to a running space that already has remote access enabled. + * + * Expected behavior: + * - tryRefreshNode() is called once at the start + * - NO confirmation dialog is shown (space is already configured correctly) + * - NO space operations are performed: + * - No deleteApp() (no need to stop) + * - No startSpace() (already running) + * - No waitForAppInService() (already ready) + * - ONLY tryRemoteConnection() is called to establish the connection + * + * This is the "happy path" - space is ready, just connect directly. + */ + it('connects directly without any space operations', async () => { + await openRemoteConnect(mockNode, {} as any, mockClient) + + // Verify tryRefreshNode was called at start + assert(mockTryRefreshNode.calledOnce) + // Verify no confirmation needed + const confirmMessages = getTestWindow().shownMessages.filter((m) => + m.message.includes('Restart Space and Connect') + ) + assert.strictEqual(confirmMessages.length, 0) + // Verify no space operations performed + assert(mockClient.deleteApp.notCalled) + assert(mockClient.startSpace.notCalled) + assert(mockClient.waitForAppInService.notCalled) + // Only remote connection should be attempted + assert(mockTryRemoteConnection.calledOnce) + }) + }) + }) +}) diff --git a/packages/core/src/test/awsService/sagemaker/credentialMapping.test.ts b/packages/core/src/test/awsService/sagemaker/credentialMapping.test.ts index 06f19a5e890..6b699dbaab4 100644 --- a/packages/core/src/test/awsService/sagemaker/credentialMapping.test.ts +++ b/packages/core/src/test/awsService/sagemaker/credentialMapping.test.ts @@ -5,10 +5,22 @@ import * as sinon from 'sinon' import * as assert from 'assert' -import { persistLocalCredentials, persistSSMConnection } from '../../../awsService/sagemaker/credentialMapping' +import { + persistLocalCredentials, + persistSSMConnection, + persistSmusProjectCreds, + loadMappings, + saveMappings, + setSpaceIamProfile, + setSpaceSsoProfile, + setSmusSpaceProfile, + setSpaceCredentials, +} from '../../../awsService/sagemaker/credentialMapping' import { Auth } from '../../../auth' import { DevSettings, fs } from '../../../shared' import globals from '../../../shared/extensionGlobals' +import { SagemakerUnifiedStudioSpaceNode } from '../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode' +import { SageMakerUnifiedStudioSpacesParentNode } from '../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode' describe('credentialMapping', () => { describe('persistLocalCredentials', () => { @@ -206,5 +218,301 @@ describe('credentialMapping', () => { 'Unsupported or missing app type for space. Expected JupyterLab or CodeEditor, got: UnsupportedApp', }) }) + + it('stores undefined refreshUrl when isSMUS=true', async () => { + sandbox.stub(DevSettings.instance, 'get').returns({}) + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + await persistSSMConnection(appArn, domain, 'sess-123', 'wss://smus-ws', 'token-xyz', 'jupyterlab', true) + + const raw = writeStub.firstCall.args[1] + const data = JSON.parse(typeof raw === 'string' ? raw : raw.toString()) + + // Verify refreshUrl is undefined for SMUS connections + assert.strictEqual(data.deepLink?.[appArn]?.refreshUrl, undefined) + + // Verify SSM connection info is stored correctly + assert.deepStrictEqual(data.deepLink?.[appArn]?.requests['initial-connection'], { + sessionId: 'sess-123', + url: 'wss://smus-ws', + token: 'token-xyz', + status: 'fresh', + }) + }) + + it('stores valid refreshUrl when isSMUS=false (SageMaker AI behavior)', async () => { + sandbox.stub(DevSettings.instance, 'get').returns({}) + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + await persistSSMConnection(appArn, domain, 'sess-456', 'wss://sm-ws', 'token-abc', 'jupyterlab', false) + + const raw = writeStub.firstCall.args[1] + const data = JSON.parse(typeof raw === 'string' ? raw : raw.toString()) + + // Verify refreshUrl is present for SageMaker AI connections + assert.ok(data.deepLink?.[appArn]?.refreshUrl) + assertRefreshUrlMatches(data.deepLink?.[appArn]?.refreshUrl, 'studio.us-west-2.sagemaker.aws') + + // Verify SSM connection info is stored correctly + assert.deepStrictEqual(data.deepLink?.[appArn]?.requests['initial-connection'], { + sessionId: 'sess-456', + url: 'wss://sm-ws', + token: 'token-abc', + status: 'fresh', + }) + }) + + it('stores valid refreshUrl when isSMUS is undefined (default SageMaker AI behavior)', async () => { + sandbox.stub(DevSettings.instance, 'get').returns({}) + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + // Call without isSMUS parameter (should default to SageMaker AI behavior) + await persistSSMConnection(appArn, domain, 'sess-789', 'wss://default-ws', 'token-def', 'jupyterlab') + + const raw = writeStub.firstCall.args[1] + const data = JSON.parse(typeof raw === 'string' ? raw : raw.toString()) + + // Verify refreshUrl is present when isSMUS is not specified + assert.ok(data.deepLink?.[appArn]?.refreshUrl) + assertRefreshUrlMatches(data.deepLink?.[appArn]?.refreshUrl, 'studio.us-west-2.sagemaker.aws') + + // Verify SSM connection info is stored correctly + assert.deepStrictEqual(data.deepLink?.[appArn]?.requests['initial-connection'], { + sessionId: 'sess-789', + url: 'wss://default-ws', + token: 'token-def', + status: 'fresh', + }) + }) + }) + + describe('persistSmusProjectCreds', () => { + const appArn = 'arn:aws:sagemaker:us-west-2:123456789012:space/d-f0lwireyzpjp/test-space' + const projectId = 'test-project-id' + let sandbox: sinon.SinonSandbox + let mockNode: sinon.SinonStubbedInstance + let mockParent: sinon.SinonStubbedInstance + + beforeEach(() => { + sandbox = sinon.createSandbox() + mockNode = sandbox.createStubInstance(SagemakerUnifiedStudioSpaceNode) + mockParent = sandbox.createStubInstance(SageMakerUnifiedStudioSpacesParentNode) + }) + + afterEach(() => { + sandbox.restore() + }) + + it('persists SMUS project credentials', async () => { + const mockCredentialProvider = { + getCredentials: sandbox.stub().resolves(), + startProactiveCredentialRefresh: sandbox.stub(), + } + + const mockAuthProvider = { + getProjectCredentialProvider: sandbox.stub().resolves(mockCredentialProvider), + } + + mockNode.getParent.returns(mockParent as any) + mockParent.getAuthProvider.returns(mockAuthProvider as any) + mockParent.getProjectId.returns(projectId) + sandbox.stub(require('../../../sagemakerunifiedstudio/auth/model'), 'isSmusSsoConnection').returns(true) + + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + await persistSmusProjectCreds(appArn, mockNode as any) + + assert.ok(writeStub.calledOnce) + const raw = writeStub.firstCall.args[1] + const data = JSON.parse(typeof raw === 'string' ? raw : raw.toString()) + assert.deepStrictEqual(data.localCredential?.[appArn], { + type: 'sso', + smusProjectId: projectId, + }) + + // Verify the correct methods were called + assert.ok(mockAuthProvider.getProjectCredentialProvider.calledWith(projectId)) + assert.ok(mockCredentialProvider.getCredentials.calledOnce) + assert.ok(mockCredentialProvider.startProactiveCredentialRefresh.calledOnce) + }) + }) + + describe('loadMappings', () => { + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + it('returns empty object when file does not exist', async () => { + sandbox.stub(fs, 'existsFile').resolves(false) + + const result = await loadMappings() + + assert.deepStrictEqual(result, {}) + }) + + it('loads and parses existing mappings', async () => { + const mockData = { localCredential: { 'test-arn': { type: 'iam' as const, profileName: 'test' } } } + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(mockData)) + + const result = await loadMappings() + + assert.deepStrictEqual(result, mockData) + }) + + it('returns empty object on parse error', async () => { + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves('invalid json') + + const result = await loadMappings() + + assert.deepStrictEqual(result, {}) + }) + }) + + describe('saveMappings', () => { + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + it('saves mappings to file', async () => { + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + const testData = { localCredential: { 'test-arn': { type: 'iam' as const, profileName: 'test' } } } + + await saveMappings(testData) + + assert.ok(writeStub.calledOnce) + const [, content, options] = writeStub.firstCall.args + assert.strictEqual(content, JSON.stringify(testData, undefined, 2)) + assert.deepStrictEqual(options, { mode: 0o600, atomic: true }) + }) + }) + + describe('setSpaceIamProfile', () => { + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + it('sets IAM profile for space', async () => { + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + await setSpaceIamProfile('test-space', 'test-profile') + + const raw = writeStub.firstCall.args[1] + const data = JSON.parse(typeof raw === 'string' ? raw : raw.toString()) + assert.deepStrictEqual(data.localCredential?.['test-space'], { + type: 'iam', + profileName: 'test-profile', + }) + }) + }) + + describe('setSpaceSsoProfile', () => { + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + it('sets SSO profile for space', async () => { + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + await setSpaceSsoProfile('test-space', 'access-key', 'secret', 'token') + + const raw = writeStub.firstCall.args[1] + const data = JSON.parse(typeof raw === 'string' ? raw : raw.toString()) + assert.deepStrictEqual(data.localCredential?.['test-space'], { + type: 'sso', + accessKey: 'access-key', + secret: 'secret', + token: 'token', + }) + }) + }) + + describe('setSmusSpaceProfile', () => { + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + it('sets SMUS SSO profile for space', async () => { + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + await setSmusSpaceProfile('test-space', 'project-id', 'sso') + + const raw = writeStub.firstCall.args[1] + const data = JSON.parse(typeof raw === 'string' ? raw : raw.toString()) + assert.deepStrictEqual(data.localCredential?.['test-space'], { + type: 'sso', + smusProjectId: 'project-id', + }) + }) + }) + + describe('setSpaceCredentials', () => { + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + it('sets space credentials with refresh URL', async () => { + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + const credentials = { sessionId: 'sess', url: 'ws://test', token: 'token' } + + await setSpaceCredentials('test-space', 'https://refresh.url', credentials) + + const raw = writeStub.firstCall.args[1] + const data = JSON.parse(typeof raw === 'string' ? raw : raw.toString()) + assert.deepStrictEqual(data.deepLink?.['test-space'], { + refreshUrl: 'https://refresh.url', + requests: { + 'initial-connection': { + ...credentials, + status: 'fresh', + }, + }, + }) + }) }) }) diff --git a/packages/core/src/test/awsService/sagemaker/detached-server/credentials.test.ts b/packages/core/src/test/awsService/sagemaker/detached-server/credentials.test.ts index a979c2186d3..3db189f8390 100644 --- a/packages/core/src/test/awsService/sagemaker/detached-server/credentials.test.ts +++ b/packages/core/src/test/awsService/sagemaker/detached-server/credentials.test.ts @@ -73,6 +73,69 @@ describe('resolveCredentialsFor', () => { }) }) + it('resolves SSO credentials with SMUS project ID', async () => { + sinon.stub(utils, 'readMapping').resolves({ + localCredential: { + [connectionId]: { + type: 'sso', + smusProjectId: 'project123', + }, + }, + smusProjects: { + project123: { + accessKey: 'smus-key', + secret: 'smus-secret', + token: 'smus-token', + }, + }, + }) + + const creds = await resolveCredentialsFor(connectionId) + assert.deepStrictEqual(creds, { + accessKeyId: 'smus-key', + secretAccessKey: 'smus-secret', + sessionToken: 'smus-token', + }) + }) + + it('throws if SMUS project credentials are missing', async () => { + sinon.stub(utils, 'readMapping').resolves({ + localCredential: { + [connectionId]: { + type: 'sso', + smusProjectId: 'project123', + }, + }, + smusProjects: { + project123: { + accessKey: '', + secret: 'smus-secret', + token: 'smus-token', + }, + }, + }) + + await assert.rejects(() => resolveCredentialsFor(connectionId), { + message: `Missing ProjectRole credentials for SMUS Space "${connectionId}"`, + }) + }) + + it('throws if SMUS project is not found', async () => { + sinon.stub(utils, 'readMapping').resolves({ + localCredential: { + [connectionId]: { + type: 'sso', + smusProjectId: 'nonexistent', + }, + }, + smusProjects: {}, + }) + + await assert.rejects(() => resolveCredentialsFor(connectionId), { + message: `Missing ProjectRole credentials for SMUS Space "${connectionId}"`, + }) + }) + it('throws for unsupported profile types', async () => { sinon.stub(utils, 'readMapping').resolves({ localCredential: { diff --git a/packages/core/src/test/awsService/sagemaker/detached-server/routes/getSessionAsync.test.ts b/packages/core/src/test/awsService/sagemaker/detached-server/routes/getSessionAsync.test.ts index 8d3ab8563ee..9b3ecb2f2c9 100644 --- a/packages/core/src/test/awsService/sagemaker/detached-server/routes/getSessionAsync.test.ts +++ b/packages/core/src/test/awsService/sagemaker/detached-server/routes/getSessionAsync.test.ts @@ -9,6 +9,8 @@ import assert from 'assert' import { SessionStore } from '../../../../../awsService/sagemaker/detached-server/sessionStore' import { handleGetSessionAsync } from '../../../../../awsService/sagemaker/detached-server/routes/getSessionAsync' import * as utils from '../../../../../awsService/sagemaker/detached-server/utils' +import * as errorPage from '../../../../../awsService/sagemaker/detached-server/errorPage' +import { SmusDeeplinkSessionExpiredError } from '../../../../../awsService/sagemaker/constants' describe('handleGetSessionAsync', () => { let req: Partial @@ -27,6 +29,7 @@ describe('handleGetSessionAsync', () => { sinon.stub(SessionStore.prototype, 'getStatus').callsFake(storeStub.getStatus) sinon.stub(SessionStore.prototype, 'getRefreshUrl').callsFake(storeStub.getRefreshUrl) sinon.stub(SessionStore.prototype, 'markPending').callsFake(storeStub.markPending) + sinon.stub(SessionStore.prototype, 'cleanupExpiredConnection').callsFake(storeStub.cleanupExpiredConnection) }) it('responds with 400 if required query parameters are missing', async () => { @@ -93,6 +96,99 @@ describe('handleGetSessionAsync', () => { assert(resEnd.calledWith('Unexpected error')) }) + describe('SMUS session expiration handling', () => { + let openErrorPageStub: sinon.SinonStub + + beforeEach(() => { + // Stub the openErrorPage function to prevent actual browser opening + openErrorPageStub = sinon.stub(errorPage, 'openErrorPage').resolves() + }) + + it('handles SMUS session expiration when refreshUrl is undefined', async () => { + req = { url: '/session_async?connection_identifier=abc&request_id=req123' } + + storeStub.getFreshEntry.returns(Promise.resolve(undefined)) + storeStub.getStatus.returns(Promise.resolve('not-started')) + storeStub.getRefreshUrl.returns(Promise.resolve(undefined)) // SMUS case: no refreshUrl + storeStub.cleanupExpiredConnection.resolves() + + await handleGetSessionAsync(req as http.IncomingMessage, res as http.ServerResponse) + + // Verify HTTP 400 response with correct error structure + assert(resWriteHead.calledWith(400)) + const actualJson = JSON.parse(resEnd.firstCall.args[0]) + assert.strictEqual(actualJson.error, SmusDeeplinkSessionExpiredError.code) + assert.strictEqual(actualJson.message, SmusDeeplinkSessionExpiredError.shortMessage) + + // Verify cleanup was called + assert(storeStub.cleanupExpiredConnection.calledOnce) + assert(storeStub.cleanupExpiredConnection.calledWith('abc')) + + // Verify error page was opened with correct message + assert(openErrorPageStub.calledOnce) + assert.strictEqual(openErrorPageStub.firstCall.args[0], SmusDeeplinkSessionExpiredError.title) + assert.strictEqual(openErrorPageStub.firstCall.args[1], SmusDeeplinkSessionExpiredError.message) + }) + + it('responds with 400 even if cleanup fails', async () => { + req = { url: '/session_async?connection_identifier=abc&request_id=req123' } + + storeStub.getFreshEntry.returns(Promise.resolve(undefined)) + storeStub.getStatus.returns(Promise.resolve('not-started')) + storeStub.getRefreshUrl.returns(Promise.resolve(undefined)) + storeStub.cleanupExpiredConnection.rejects(new Error('cleanup failed')) + + await handleGetSessionAsync(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(400)) + const actualJson = JSON.parse(resEnd.firstCall.args[0]) + assert.strictEqual(actualJson.error, SmusDeeplinkSessionExpiredError.code) + }) + + it('responds with 202 when refreshUrl is valid (existing SageMaker AI flow)', async () => { + req = { url: '/session_async?connection_identifier=abc&request_id=req123' } + + storeStub.getFreshEntry.returns(Promise.resolve(undefined)) + storeStub.getStatus.returns(Promise.resolve('not-started')) + storeStub.getRefreshUrl.returns(Promise.resolve('https://example.com/refresh')) // Valid refreshUrl + storeStub.markPending.returns(Promise.resolve()) + + sinon.stub(utils, 'readServerInfo').resolves({ pid: 1234, port: 4567 }) + sinon + .stub(utils, 'parseArn') + .returns({ region: 'us-east-1', accountId: '123456789012', spaceName: 'test-space' }) + sinon.stub(utils, 'open').resolves() + + await handleGetSessionAsync(req as http.IncomingMessage, res as http.ServerResponse) + + // Verify SageMaker AI flow still works correctly + assert(resWriteHead.calledWith(202)) + assert(resEnd.calledWithMatch(/Session is not ready yet/)) + assert(storeStub.markPending.calledWith('abc', 'req123')) + }) + + it('does not call cleanupExpiredConnection for SageMaker AI connections', async () => { + req = { url: '/session_async?connection_identifier=abc&request_id=req123' } + + storeStub.getFreshEntry.returns(Promise.resolve(undefined)) + storeStub.getStatus.returns(Promise.resolve('not-started')) + storeStub.getRefreshUrl.returns(Promise.resolve('https://example.com/refresh')) + storeStub.markPending.returns(Promise.resolve()) + storeStub.cleanupExpiredConnection.resolves() + + sinon.stub(utils, 'readServerInfo').resolves({ pid: 1234, port: 4567 }) + sinon + .stub(utils, 'parseArn') + .returns({ region: 'us-east-1', accountId: '123456789012', spaceName: 'test-space' }) + sinon.stub(utils, 'open').resolves() + + await handleGetSessionAsync(req as http.IncomingMessage, res as http.ServerResponse) + + // Verify cleanup was NOT called + assert(storeStub.cleanupExpiredConnection.notCalled) + }) + }) + afterEach(() => { sinon.restore() }) diff --git a/packages/core/src/test/awsService/sagemaker/detached-server/sessionStore.test.ts b/packages/core/src/test/awsService/sagemaker/detached-server/sessionStore.test.ts index 2a7828a4951..468b92faa15 100644 --- a/packages/core/src/test/awsService/sagemaker/detached-server/sessionStore.test.ts +++ b/packages/core/src/test/awsService/sagemaker/detached-server/sessionStore.test.ts @@ -40,6 +40,28 @@ describe('SessionStore', () => { assert.strictEqual(result, 'https://refresh.url') }) + it('returns undefined for SMUS connections (no refreshUrl)', async () => { + const store = new SessionStore() + readMappingStub.returns({ + deepLink: { + [connectionId]: { + refreshUrl: undefined, + requests: { + 'initial-connection': { sessionId: 's0', token: 't0', url: 'u0', status: 'fresh' }, + }, + }, + }, + }) + const result = await store.getRefreshUrl(connectionId) + assert.strictEqual(result, undefined) + }) + + it('returns valid URL for SageMaker AI connections (existing behavior)', async () => { + const store = new SessionStore() + const result = await store.getRefreshUrl(connectionId) + assert.strictEqual(result, 'https://refresh.url') + }) + it('throws if no mapping exists for connectionId', async () => { const store = new SessionStore() readMappingStub.returns({ deepLink: {} }) @@ -47,6 +69,13 @@ describe('SessionStore', () => { await assert.rejects(() => store.getRefreshUrl('missing'), /No mapping found/) }) + it('throws if no deepLink mapping exists', async () => { + const store = new SessionStore() + readMappingStub.returns({}) + + await assert.rejects(() => store.getRefreshUrl(connectionId), /No deepLink mapping found/) + }) + it('returns fresh entry and marks consumed', async () => { const store = new SessionStore() const result = await store.getFreshEntry(connectionId, requestId) @@ -142,4 +171,44 @@ describe('SessionStore', () => { status: 'fresh', }) }) + + it('cleans up expired connection', async () => { + const store = new SessionStore() + await store.cleanupExpiredConnection(connectionId) + const updated = writeMappingStub.firstCall.args[0] + assert.strictEqual(updated.deepLink[connectionId], undefined) + }) + + it('does not throw when cleaning up non-existent connection', async () => { + const store = new SessionStore() + await store.cleanupExpiredConnection('non-existent-connection') + assert(writeMappingStub.notCalled) + }) + + it('cleans up only the specified connection without affecting other connections', async () => { + const store = new SessionStore() + const otherConnectionId = 'other-connection' + readMappingStub.returns({ + deepLink: { + [connectionId]: { + refreshUrl: undefined, + requests: { + 'initial-connection': { sessionId: 's1', token: 't1', url: 'u1', status: 'fresh' }, + }, + }, + [otherConnectionId]: { + refreshUrl: 'https://refresh.url', + requests: { + 'initial-connection': { sessionId: 's2', token: 't2', url: 'u2', status: 'fresh' }, + }, + }, + }, + }) + + await store.cleanupExpiredConnection(connectionId) + const updated = writeMappingStub.firstCall.args[0] + assert.strictEqual(updated.deepLink[connectionId], undefined) + assert.ok(updated.deepLink[otherConnectionId]) + assert.ok(updated.deepLink[otherConnectionId].requests['initial-connection']) + }) }) diff --git a/packages/core/src/test/awsService/sagemaker/explorer/sagemakerSpaceNode.test.ts b/packages/core/src/test/awsService/sagemaker/explorer/sagemakerSpaceNode.test.ts index 57b4d7a80c6..5289dea266d 100644 --- a/packages/core/src/test/awsService/sagemaker/explorer/sagemakerSpaceNode.test.ts +++ b/packages/core/src/test/awsService/sagemaker/explorer/sagemakerSpaceNode.test.ts @@ -9,13 +9,13 @@ import assert from 'assert' import { AppType } from '@aws-sdk/client-sagemaker' import { SagemakerClient, SagemakerSpaceApp } from '../../../../shared/clients/sagemaker' import { SagemakerSpaceNode } from '../../../../awsService/sagemaker/explorer/sagemakerSpaceNode' -import { SagemakerParentNode } from '../../../../awsService/sagemaker/explorer/sagemakerParentNode' +import { SagemakerStudioNode } from '../../../../awsService/sagemaker/explorer/sagemakerStudioNode' import { PollingSet } from '../../../../shared/utilities/pollingSet' describe('SagemakerSpaceNode', function () { const testRegion = 'testRegion' let client: SagemakerClient - let testParent: SagemakerParentNode + let testParent: SagemakerStudioNode let testSpaceApp: SagemakerSpaceApp let describeAppStub: sinon.SinonStub let testSpaceAppNode: SagemakerSpaceNode @@ -34,7 +34,7 @@ describe('SagemakerSpaceNode', function () { sinon.stub(PollingSet.prototype, 'add') client = new SagemakerClient(testRegion) - testParent = new SagemakerParentNode(testRegion, client) + testParent = new SagemakerStudioNode(testRegion, client) describeAppStub = sinon.stub(SagemakerClient.prototype, 'describeApp') testSpaceAppNode = new SagemakerSpaceNode(testParent, client, testRegion, testSpaceApp) @@ -69,10 +69,9 @@ describe('SagemakerSpaceNode', function () { }) it('returns ARN from describeApp', async function () { - describeAppStub.resolves({ AppArn: 'arn:aws:sagemaker:1234:app/TestApp' }) + describeAppStub.resolves({ AppArn: 'arn:aws:sagemaker:1234:app/TestApp', $metadata: {} }) - const node = new SagemakerSpaceNode(testParent, client, testRegion, testSpaceApp) - const arn = await node.getAppArn() + const arn = await testSpaceAppNode.getAppArn() assert.strictEqual(arn, 'arn:aws:sagemaker:1234:app/TestApp') sinon.assert.calledOnce(describeAppStub) @@ -84,10 +83,42 @@ describe('SagemakerSpaceNode', function () { }) }) - it('updates status with new spaceApp', async function () { - const newStatus = 'Starting' + it('returns space ARN from describeSpace', async function () { + const describeSpaceStub = sinon.stub(SagemakerClient.prototype, 'describeSpace') + describeSpaceStub.resolves({ SpaceArn: 'arn:aws:sagemaker:1234:space/TestSpace', $metadata: {} }) + + const arn = await testSpaceAppNode.getSpaceArn() + + assert.strictEqual(arn, 'arn:aws:sagemaker:1234:space/TestSpace') + sinon.assert.calledOnce(describeSpaceStub) + }) + + it('updates status with new spaceApp', function () { const newSpaceApp = { ...testSpaceApp, App: { AppName: 'TestApp', Status: 'Pending' } } as SagemakerSpaceApp testSpaceAppNode.updateSpace(newSpaceApp) - assert.strictEqual(testSpaceAppNode.getStatus(), newStatus) + assert.strictEqual(testSpaceAppNode.getStatus(), 'Starting') + }) + + it('delegates to SagemakerSpace for properties', function () { + const node = new SagemakerSpaceNode(testParent, client, testRegion, testSpaceApp) + + // Verify that properties are managed by SagemakerSpace + assert.strictEqual(node.name, 'TestSpace') + assert.strictEqual(node.label, 'TestSpace (Running)') + assert.strictEqual(node.description, 'Private space') + assert.ok(node.tooltip instanceof vscode.MarkdownString) + }) + + it('updates space app status', async function () { + const describeSpaceStub = sinon.stub(SagemakerClient.prototype, 'describeSpace') + describeSpaceStub.resolves({ SpaceName: 'TestSpace', Status: 'InService', $metadata: {} }) + + const listAppForSpaceStub = sinon.stub(SagemakerClient.prototype, 'listAppForSpace') + listAppForSpaceStub.resolves({ AppName: 'TestApp', Status: 'InService' }) + + await testSpaceAppNode.updateSpaceAppStatus() + + sinon.assert.calledOnce(describeSpaceStub) + sinon.assert.calledOnce(listAppForSpaceStub) }) }) diff --git a/packages/core/src/test/awsService/sagemaker/explorer/sagemakerParentNode.test.ts b/packages/core/src/test/awsService/sagemaker/explorer/sagemakerStudioNode.test.ts similarity index 96% rename from packages/core/src/test/awsService/sagemaker/explorer/sagemakerParentNode.test.ts rename to packages/core/src/test/awsService/sagemaker/explorer/sagemakerStudioNode.test.ts index 8fccfe4bfd9..6eabd961356 100644 --- a/packages/core/src/test/awsService/sagemaker/explorer/sagemakerParentNode.test.ts +++ b/packages/core/src/test/awsService/sagemaker/explorer/sagemakerStudioNode.test.ts @@ -10,20 +10,20 @@ import { GetCallerIdentityResponse } from 'aws-sdk/clients/sts' import { SagemakerClient, SagemakerSpaceApp } from '../../../../shared/clients/sagemaker' import { SagemakerConstants } from '../../../../awsService/sagemaker/explorer/constants' import { - SagemakerParentNode, + SagemakerStudioNode, SelectedDomainUsers, SelectedDomainUsersByRegion, -} from '../../../../awsService/sagemaker/explorer/sagemakerParentNode' +} from '../../../../awsService/sagemaker/explorer/sagemakerStudioNode' import { globals } from '../../../../shared' import { DefaultStsClient } from '../../../../shared/clients/stsClient' import { assertNodeListOnlyHasPlaceholderNode } from '../../../utilities/explorerNodeAssertions' import assert from 'assert' -describe('sagemakerParentNode', function () { - let testNode: SagemakerParentNode +describe('sagemakerStudioNode', function () { + let testNode: SagemakerStudioNode let client: SagemakerClient let fetchSpaceAppsAndDomainsStub: sinon.SinonStub< - [], + [domainId?: string | undefined, filterSmusDomains?: boolean | undefined], Promise<[Map, Map]> > let getCallerIdentityStub: sinon.SinonStub<[], Promise> @@ -111,7 +111,7 @@ describe('sagemakerParentNode', function () { beforeEach(function () { fetchSpaceAppsAndDomainsStub = sinon.stub(SagemakerClient.prototype, 'fetchSpaceAppsAndDomains') getCallerIdentityStub = sinon.stub(DefaultStsClient.prototype, 'getCallerIdentity') - testNode = new SagemakerParentNode(testRegion, client) + testNode = new SagemakerStudioNode(testRegion, client) }) afterEach(function () { @@ -194,7 +194,7 @@ describe('sagemakerParentNode', function () { let originalState: Map beforeEach(async function () { - testNode = new SagemakerParentNode(testRegion, client) + testNode = new SagemakerStudioNode(testRegion, client) originalState = new Map( globals.globalState.get(SagemakerConstants.SelectedDomainUsersState, []) ) @@ -242,7 +242,7 @@ describe('sagemakerParentNode', function () { let originalState: Map beforeEach(async function () { - testNode = new SagemakerParentNode(testRegion, client) + testNode = new SagemakerStudioNode(testRegion, client) originalState = new Map( globals.globalState.get(SagemakerConstants.SelectedDomainUsersState, []) ) @@ -277,7 +277,7 @@ describe('sagemakerParentNode', function () { }) beforeEach(function () { - testNode = new SagemakerParentNode(testRegion, client) + testNode = new SagemakerStudioNode(testRegion, client) }) it('matches IAM user ARN when filtering is enabled', async function () { diff --git a/packages/core/src/test/awsService/sagemaker/model.test.ts b/packages/core/src/test/awsService/sagemaker/model.test.ts index 892baf2f77b..7570e1bfe31 100644 --- a/packages/core/src/test/awsService/sagemaker/model.test.ts +++ b/packages/core/src/test/awsService/sagemaker/model.test.ts @@ -59,6 +59,30 @@ describe('SageMaker Model', () => { assert.ok(existsStub.callCount >= 3, 'should have retried for file existence') }) + + it('throws ToolkitError when info file never appears', async function () { + sandbox.stub(fs, 'existsFile').resolves(false) + sandbox.stub(require('fs'), 'openSync').returns(42) + sandbox.replace( + require('../../../awsService/sagemaker/model'), + 'stopLocalServer', + sandbox.stub().resolves() + ) + sandbox.replace( + require('../../../awsService/sagemaker/utils'), + 'spawnDetachedServer', + sandbox.stub().returns({ unref: sandbox.stub() }) + ) + sandbox.stub(DevSettings.instance, 'get').returns({}) + + try { + await startLocalServer(ctx) + assert.ok(false, 'Expected error not thrown') + } catch (err) { + assert.ok(err instanceof ToolkitError) + assert.ok(err.message.includes('Timed out waiting for local server info file')) + } + }) }) describe('stopLocalServer', function () { @@ -106,6 +130,17 @@ describe('SageMaker Model', () => { } }) + it('logs warning when process not found (ESRCH)', async function () { + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(validJson) + sandbox.stub(fs, 'delete').resolves() + sandbox.stub(process, 'kill').throws({ code: 'ESRCH', message: 'no such process' }) + + await stopLocalServer(ctx) + + assertLogsContain(`no process found with PID ${validPid}. It may have already exited.`, false, 'warn') + }) + it('throws ToolkitError when killing process fails for another reason', async function () { sandbox.stub(fs, 'existsFile').resolves(true) sandbox.stub(fs, 'readFileText').resolves(validJson) @@ -120,6 +155,27 @@ describe('SageMaker Model', () => { assert.strictEqual(err.message, 'failed to stop local server') } }) + + it('logs warning when PID is invalid', async function () { + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify({ pid: 'invalid' })) + sandbox.stub(fs, 'delete').resolves() + + await stopLocalServer(ctx) + + assertLogsContain('no valid PID found in info file.', false, 'warn') + }) + + it('logs warning when file deletion fails', async function () { + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(validJson) + sandbox.stub(process, 'kill').returns(true) + sandbox.stub(fs, 'delete').rejects(new Error('delete failed')) + + await stopLocalServer(ctx) + + assertLogsContain('could not delete info file: delete failed', false, 'warn') + }) }) describe('removeKnownHost', function () { @@ -152,6 +208,60 @@ describe('SageMaker Model', () => { sinon.match((value: string) => value.trim() === expectedOutput), { atomic: true } ) + assertLogsContain(`Removed '${hostname}' from known_hosts`, false, 'debug') + }) + + it('removes case-sensitive hostname when entry in known_hosts is lowercase', async function () { + const mixedCaseHostname = 'Test.Host.Com' + + sandbox.stub(fs, 'existsFile').resolves(true) + + const inputContent = `test.host.com ssh-rsa AAAA\nsome.other.com ssh-rsa BBBB` + const expectedOutput = `some.other.com ssh-rsa BBBB` + + sandbox.stub(fs, 'readFileText').resolves(inputContent) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + await removeKnownHost(mixedCaseHostname) + + sinon.assert.calledWith( + writeStub, + path.join(os.homedir(), '.ssh', 'known_hosts'), + sinon.match((value: string) => value.trim() === expectedOutput), + { atomic: true } + ) + assertLogsContain(`Removed '${mixedCaseHostname}' from known_hosts`, false, 'debug') + }) + + it('handles hostname in comma-separated list', async function () { + sandbox.stub(fs, 'existsFile').resolves(true) + + const inputContent = `host1,${hostname},host2 ssh-rsa AAAA\nother.host ssh-rsa BBBB` + const expectedOutput = `other.host ssh-rsa BBBB` + + sandbox.stub(fs, 'readFileText').resolves(inputContent) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + await removeKnownHost(hostname) + + sinon.assert.calledWith( + writeStub, + knownHostsPath, + sinon.match((value: string) => value.trim() === expectedOutput), + { atomic: true } + ) + }) + + it('does not write file when hostname not found', async function () { + sandbox.stub(fs, 'existsFile').resolves(true) + + const inputContent = `other.host ssh-rsa AAAA\nsome.other.com ssh-rsa BBBB` + sandbox.stub(fs, 'readFileText').resolves(inputContent) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + await removeKnownHost(hostname) + + sinon.assert.notCalled(writeStub) }) it('logs warning when known_hosts does not exist', async function () { diff --git a/packages/core/src/test/awsService/sagemaker/sagemakerSpace.test.ts b/packages/core/src/test/awsService/sagemaker/sagemakerSpace.test.ts new file mode 100644 index 00000000000..12db8b9c6f6 --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/sagemakerSpace.test.ts @@ -0,0 +1,161 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { SagemakerSpace } from '../../../awsService/sagemaker/sagemakerSpace' +import { SagemakerClient, SagemakerSpaceApp } from '../../../shared/clients/sagemaker' +import sinon from 'sinon' + +describe('SagemakerSpace', function () { + let mockClient: sinon.SinonStubbedInstance + let mockSpaceApp: SagemakerSpaceApp + + beforeEach(function () { + mockClient = sinon.createStubInstance(SagemakerClient) + mockSpaceApp = { + SpaceName: 'test-space', + Status: 'InService', + DomainId: 'test-domain', + DomainSpaceKey: 'test-key', + SpaceSettingsSummary: { + AppType: 'JupyterLab', + RemoteAccess: 'ENABLED', + }, + } + }) + + afterEach(function () { + sinon.restore() + }) + + describe('updateSpaceAppStatus', function () { + it('should correctly map DescribeSpace API response to SagemakerSpaceApp type', async function () { + // Mock DescribeSpace response (uses full property names) + const mockDescribeSpaceResponse = { + SpaceName: 'updated-space', + Status: 'InService', + DomainId: 'test-domain', + SpaceSettings: { + // Note: 'SpaceSettings' not 'SpaceSettingsSummary' + AppType: 'CodeEditor', + RemoteAccess: 'DISABLED', + }, + OwnershipSettings: { + OwnerUserProfileName: 'test-user', + }, + SpaceSharingSettings: { + SharingType: 'Private', + }, + $metadata: { requestId: 'test-request-id' }, + } + + // Mock DescribeApp response + const mockDescribeAppResponse = { + AppName: 'test-app', + Status: 'InService', + ResourceSpec: { + InstanceType: 'ml.t3.medium', + }, + $metadata: { requestId: 'test-request-id' }, + } + + mockClient.describeSpace.resolves(mockDescribeSpaceResponse) + mockClient.describeApp.resolves(mockDescribeAppResponse) + mockClient.listAppForSpace.resolves(mockDescribeAppResponse) + + const space = new SagemakerSpace(mockClient as any, 'us-east-1', mockSpaceApp) + const updateSpaceSpy = sinon.spy(space, 'updateSpace') + + await space.updateSpaceAppStatus() + + // Verify updateSpace was called with correctly mapped properties + assert.ok(updateSpaceSpy.calledOnce) + const updateSpaceArgs = updateSpaceSpy.getCall(0).args[0] + + // Verify property name mapping from DescribeSpace to SagemakerSpaceApp + assert.strictEqual(updateSpaceArgs.SpaceSettingsSummary?.AppType, 'CodeEditor') + assert.strictEqual(updateSpaceArgs.SpaceSettingsSummary?.RemoteAccess, 'DISABLED') + assert.strictEqual(updateSpaceArgs.OwnershipSettingsSummary?.OwnerUserProfileName, 'test-user') + assert.strictEqual(updateSpaceArgs.SpaceSharingSettingsSummary?.SharingType, 'Private') + + // Verify other properties are preserved + assert.strictEqual(updateSpaceArgs.SpaceName, 'updated-space') + assert.strictEqual(updateSpaceArgs.Status, 'InService') + assert.strictEqual(updateSpaceArgs.DomainId, 'test-domain') + assert.strictEqual(updateSpaceArgs.App, mockDescribeAppResponse) + assert.strictEqual(updateSpaceArgs.DomainSpaceKey, 'test-key') + + // Verify original API property names are not present + assert.ok(!('SpaceSettings' in updateSpaceArgs)) + assert.ok(!('OwnershipSettings' in updateSpaceArgs)) + assert.ok(!('SpaceSharingSettings' in updateSpaceArgs)) + }) + + it('should handle missing optional properties gracefully', async function () { + // Mock minimal DescribeSpace response + const mockDescribeSpaceResponse = { + SpaceName: 'minimal-space', + Status: 'InService', + DomainId: 'test-domain', + $metadata: { requestId: 'test-request-id' }, + // No SpaceSettings, OwnershipSettings, or SpaceSharingSettings + } + + const mockDescribeAppResponse = { + AppName: 'test-app', + Status: 'InService', + $metadata: { requestId: 'test-request-id' }, + } + mockClient.listAppForSpace.resolves(mockDescribeAppResponse) + mockClient.describeSpace.resolves(mockDescribeSpaceResponse) + + const space = new SagemakerSpace(mockClient as any, 'us-east-1', mockSpaceApp) + const updateSpaceSpy = sinon.spy(space, 'updateSpace') + + await space.updateSpaceAppStatus() + + // Should not throw and should handle undefined properties + assert.ok(updateSpaceSpy.calledOnce) + const updateSpaceArgs = updateSpaceSpy.getCall(0).args[0] + + assert.strictEqual(updateSpaceArgs.SpaceName, 'minimal-space') + assert.strictEqual(updateSpaceArgs.SpaceSettingsSummary, undefined) + assert.strictEqual(updateSpaceArgs.OwnershipSettingsSummary, undefined) + assert.strictEqual(updateSpaceArgs.SpaceSharingSettingsSummary, undefined) + }) + + it('should update app status using listAppForSpace', async function () { + const mockDescribeSpaceResponse = { + SpaceName: 'test-space', + Status: 'InService', + DomainId: 'test-domain', + $metadata: { requestId: 'test-request-id' }, + } + + const mockAppFromList = { + AppName: 'listed-app', + Status: 'InService', + $metadata: { requestId: 'test-request-id' }, + } + + mockClient.describeSpace.resolves(mockDescribeSpaceResponse) + mockClient.listAppForSpace.resolves(mockAppFromList) + + // Create space without App.AppName + const spaceWithoutAppName: SagemakerSpaceApp = { + ...mockSpaceApp, + App: undefined, + } + + const space = new SagemakerSpace(mockClient as any, 'us-east-1', spaceWithoutAppName) + await space.updateSpaceAppStatus() + + // Verify listAppForSpace was called instead of describeApp + assert.ok(mockClient.listAppForSpace.calledOnce) + assert.ok(mockClient.listAppForSpace.calledWith('test-domain', 'test-space')) + assert.ok(mockClient.describeApp.notCalled) + }) + }) +}) diff --git a/packages/core/src/test/awsService/sagemaker/uriHandlers.test.ts b/packages/core/src/test/awsService/sagemaker/uriHandlers.test.ts index 9ff24b2a3f9..f27df1fcb11 100644 --- a/packages/core/src/test/awsService/sagemaker/uriHandlers.test.ts +++ b/packages/core/src/test/awsService/sagemaker/uriHandlers.test.ts @@ -44,6 +44,7 @@ describe('SageMaker URI handler', function () { ws_url: 'wss://example.com', 'cell-number': '4', token: 'my-token', + app_type: 'jupyterlab', } const uri = createConnectUri(params) @@ -55,5 +56,24 @@ describe('SageMaker URI handler', function () { assert.deepStrictEqual(deeplinkConnectStub.firstCall.args[3], 'wss://example.com&cell-number=4') assert.deepStrictEqual(deeplinkConnectStub.firstCall.args[4], 'my-token') assert.deepStrictEqual(deeplinkConnectStub.firstCall.args[5], 'my-domain') + assert.deepStrictEqual(deeplinkConnectStub.firstCall.args[6], 'jupyterlab') + }) + + it('calls deeplinkConnect with undefined app_type when not provided', async function () { + const params = { + connection_identifier: 'abc123', + domain: 'my-domain', + user_profile: 'me', + session: 'sess-xyz', + ws_url: 'wss://example.com', + 'cell-number': '4', + token: 'my-token', + } + + const uri = createConnectUri(params) + await handler.handleUri(uri) + + assert.ok(deeplinkConnectStub.calledOnce) + assert.deepStrictEqual(deeplinkConnectStub.firstCall.args[6], undefined) }) }) diff --git a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts index 05164274b70..a57ff6fcea3 100644 --- a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts +++ b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts @@ -43,7 +43,6 @@ import { createManageSubscription, createOpenReferenceLog, createReconnect, - createSecurityScan, createSelectCustomization, createSeparator, createSettingsNode, @@ -506,7 +505,6 @@ describe('CodeWhisperer-basicCommands', function () { createOpenReferenceLog(), createGettingStarted(), createSeparator('Code Reviews'), - createSecurityScan(), createSeparator('Other Features'), switchToAmazonQNode(), createSeparator('Connect / Help'), diff --git a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts index 8d2017100b9..f3ae2fbceff 100644 --- a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts +++ b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts @@ -6,7 +6,7 @@ import assert, { fail } from 'assert' import * as vscode from 'vscode' import * as sinon from 'sinon' -import { DB, transformByQState, TransformByQStoppedError } from '../../../codewhisperer/models/model' +import { DB, JDKVersion, transformByQState, TransformByQStoppedError } from '../../../codewhisperer/models/model' import { stopTransformByQ, finalizeTransformationJob } from '../../../codewhisperer/commands/startTransformByQ' import { HttpResponse } from 'aws-sdk' import * as codeWhisperer from '../../../codewhisperer/client/codewhisperer' @@ -65,9 +65,10 @@ dependencyManagement: targetVersion: "3.0.0" originType: "THIRD_PARTY" plugins: - - identifier: "com.example:plugin" + - identifier: "plugin.id" targetVersion: "1.2.0" - versionProperty: "plugin.version" # Optional` + versionProperty: "plugin.version" # Optional + originType: "FIRST_PARTY" # or "THIRD_PARTY"` const validSctFile = ` @@ -282,6 +283,8 @@ dependencyManagement: it(`WHEN update job history called THEN returns details of last run job`, async function () { transformByQState.setJobId('abc-123') + transformByQState.setSourceJDKVersion(JDKVersion.JDK8) + transformByQState.setTargetJDKVersion(JDKVersion.JDK17) transformByQState.setProjectName('test-project') transformByQState.setPolledJobStatus('COMPLETED') transformByQState.setStartTime('05/03/24, 11:35 AM') @@ -502,6 +505,10 @@ dependencyManagement: await fs.mkdir(gitFolder) await fs.writeFile(path.join(gitFolder, 'config'), 'sample content for the test file') + const githubFolder = path.join(tempDir, '.github') + await fs.mkdir(githubFolder) + await fs.writeFile(path.join(githubFolder, 'config'), 'more sample content for the test file') + const zippedFiles = getFilesRecursively(tempDir, false) assert.strictEqual(zippedFiles.length, 1) }) @@ -570,15 +577,45 @@ dependencyManagement: assert.strictEqual(expectedWarning, warningMessage) }) - it(`WHEN validateCustomVersionsFile on fully valid .yaml file THEN passes validation`, async function () { - const missingKey = await validateCustomVersionsFile(validCustomVersionsFile) - assert.strictEqual(missingKey, undefined) + it(`WHEN validateCustomVersionsFile on fully valid .yaml file THEN passes validation`, function () { + const errorMessage = validateCustomVersionsFile(validCustomVersionsFile) + assert.strictEqual(errorMessage, undefined) }) - it(`WHEN validateCustomVersionsFile on invalid .yaml file THEN fails validation`, async function () { + it(`WHEN validateCustomVersionsFile on .yaml file with missing key THEN fails validation`, function () { const invalidFile = validCustomVersionsFile.replace('dependencyManagement', 'invalidKey') - const missingKey = await validateCustomVersionsFile(invalidFile) - assert.strictEqual(missingKey, 'dependencyManagement') + const errorMessage = validateCustomVersionsFile(invalidFile) + assert.strictEqual(errorMessage, `Missing required key: \`dependencyManagement\``) + }) + + it(`WHEN validateCustomVersionsFile on .yaml file with invalid dependency identifier format THEN fails validation`, function () { + const invalidFile = validCustomVersionsFile.replace('com.example:library1', 'com.example-library1') + const errorMessage = validateCustomVersionsFile(invalidFile) + assert.strictEqual( + errorMessage, + `Invalid dependency identifier format: \`com.example-library1\`. Must be in format \`groupId:artifactId\` without spaces` + ) + }) + + it(`WHEN validateCustomVersionsFile on .yaml file with missing plugin identifier format THEN fails validation`, function () { + const invalidFile = validCustomVersionsFile.replace('plugin.id', '') + const errorMessage = validateCustomVersionsFile(invalidFile) + assert.strictEqual(errorMessage, 'Missing `identifier` in plugin') + }) + + it(`WHEN validateCustomVersionsFile on .yaml file with invalid originType THEN fails validation`, function () { + const invalidFile = validCustomVersionsFile.replace('FIRST_PARTY', 'INVALID_TYPE') + const errorMessage = validateCustomVersionsFile(invalidFile) + assert.strictEqual( + errorMessage, + `Invalid originType: \`INVALID_TYPE\`. Must be either \`FIRST_PARTY\` or \`THIRD_PARTY\`` + ) + }) + + it(`WHEN validateCustomVersionsFile on .yaml file with missing targetVersion THEN fails validation`, function () { + const invalidFile = validCustomVersionsFile.replace('targetVersion: "2.1.0"', '') + const errorMessage = validateCustomVersionsFile(invalidFile) + assert.strictEqual(errorMessage, `Missing \`targetVersion\` in: \`com.example:library1\``) }) it(`WHEN validateMetadataFile on fully valid .sct file THEN passes validation`, async function () { diff --git a/packages/core/src/test/codewhisperer/startSecurityScan.test.ts b/packages/core/src/test/codewhisperer/startSecurityScan.test.ts index 551949aa3ab..e45bee1b7fa 100644 --- a/packages/core/src/test/codewhisperer/startSecurityScan.test.ts +++ b/packages/core/src/test/codewhisperer/startSecurityScan.test.ts @@ -17,14 +17,7 @@ import { assertTelemetry, closeAllEditors, getFetchStubWithResponse } from '../t import { AWSError } from 'aws-sdk' import { getTestWindow } from '../shared/vscode/window' import { SeverityLevel } from '../shared/vscode/message' -import { cancel } from '../../shared/localizedText' -import { - showScannedFilesMessage, - stopScanMessage, - CodeAnalysisScope, - monthlyLimitReachedNotification, - scansLimitReachedErrorMessage, -} from '../../codewhisperer/models/constants' +import { showScannedFilesMessage, CodeAnalysisScope } from '../../codewhisperer/models/constants' import * as model from '../../codewhisperer/models/model' import * as errors from '../../shared/errors' import * as timeoutUtils from '../../shared/utilities/timeoutUtils' @@ -124,70 +117,6 @@ describe('startSecurityScan', function () { }) }) - it('Should stop security scan for project scans when confirmed', async function () { - getFetchStubWithResponse({ status: 200, statusText: 'testing stub' }) - const securityScanRenderSpy = sinon.spy(diagnosticsProvider, 'initSecurityScanRender') - const securityScanStoppedErrorSpy = sinon.spy(model, 'CodeScanStoppedError') - const testWindow = getTestWindow() - testWindow.onDidShowMessage((message) => { - if (message.message === stopScanMessage) { - message.selectItem(startSecurityScan.stopScanButton) - } - }) - model.codeScanState.setToRunning() - const scanPromise = startSecurityScan.startSecurityScan( - mockSecurityPanelViewProvider, - editor, - createClient(), - extensionContext, - CodeAnalysisScope.PROJECT, - false - ) - await startSecurityScan.confirmStopSecurityScan( - model.codeScanState, - false, - CodeAnalysisScope.PROJECT, - undefined - ) - await scanPromise - assert.ok(securityScanRenderSpy.notCalled) - assert.ok(securityScanStoppedErrorSpy.calledOnce) - const warnings = testWindow.shownMessages.filter((m) => m.severity === SeverityLevel.Warning) - assert.ok(warnings.map((m) => m.message).includes(stopScanMessage)) - }) - - it('Should not stop security scan for project scans when not confirmed', async function () { - getFetchStubWithResponse({ status: 200, statusText: 'testing stub' }) - const securityScanRenderSpy = sinon.spy(diagnosticsProvider, 'initSecurityScanRender') - const securityScanStoppedErrorSpy = sinon.spy(model, 'CodeScanStoppedError') - const testWindow = getTestWindow() - testWindow.onDidShowMessage((message) => { - if (message.message === stopScanMessage) { - message.selectItem(cancel) - } - }) - model.codeScanState.setToRunning() - const scanPromise = startSecurityScan.startSecurityScan( - mockSecurityPanelViewProvider, - editor, - createClient(), - extensionContext, - CodeAnalysisScope.PROJECT, - false - ) - await startSecurityScan.confirmStopSecurityScan( - model.codeScanState, - false, - CodeAnalysisScope.PROJECT, - undefined - ) - await scanPromise - assert.ok(securityScanRenderSpy.calledOnce) - assert.ok(securityScanStoppedErrorSpy.notCalled) - const warnings = testWindow.shownMessages.filter((m) => m.severity === SeverityLevel.Warning) - assert.ok(warnings.map((m) => m.message).includes(stopScanMessage)) - }) - it('Should stop security scan for auto file scans if setting is disabled', async function () { getFetchStubWithResponse({ status: 200, statusText: 'testing stub' }) const securityScanRenderSpy = sinon.spy(diagnosticsProvider, 'initSecurityScanRender') @@ -272,39 +201,6 @@ describe('startSecurityScan', function () { ]) }) - it('Should not cancel a project scan if a file scan has started', async function () { - getFetchStubWithResponse({ status: 200, statusText: 'testing stub' }) - await model.CodeScansState.instance.setScansEnabled(true) - - const scanPromise = startSecurityScan.startSecurityScan( - mockSecurityPanelViewProvider, - editor, - createClient(), - extensionContext, - CodeAnalysisScope.PROJECT, - false - ) - await startSecurityScan.startSecurityScan( - mockSecurityPanelViewProvider, - editor, - createClient(), - extensionContext, - CodeAnalysisScope.FILE_AUTO, - false - ) - await scanPromise - assertTelemetry('codewhisperer_securityScan', [ - { - result: 'Succeeded', - codewhispererCodeScanScope: 'FILE_AUTO', - }, - { - result: 'Succeeded', - codewhispererCodeScanScope: 'PROJECT', - }, - ]) - }) - it('Should handle failed scan job status', async function () { getFetchStubWithResponse({ status: 200, statusText: 'testing stub' }) @@ -330,36 +226,6 @@ describe('startSecurityScan', function () { }) }) - it('Should show notification when throttled for project scans', async function () { - getFetchStubWithResponse({ status: 200, statusText: 'testing stub' }) - const mockClient = createClient() - mockClient.createCodeScan.throws({ - code: 'ThrottlingException', - time: new Date(), - name: 'error name', - message: scansLimitReachedErrorMessage, - } satisfies AWSError) - sinon.stub(errors, 'isAwsError').returns(true) - const testWindow = getTestWindow() - await startSecurityScan.startSecurityScan( - mockSecurityPanelViewProvider, - editor, - mockClient, - extensionContext, - CodeAnalysisScope.PROJECT, - false - ) - - assert.ok(testWindow.shownMessages.map((m) => m.message).includes(monthlyLimitReachedNotification)) - assertTelemetry('codewhisperer_securityScan', { - codewhispererCodeScanScope: 'PROJECT', - result: 'Failed', - reason: 'ThrottlingException', - reasonDesc: `ThrottlingException: Maximum com.amazon.aws.codewhisperer.StartCodeAnalysis reached for this month.`, - passive: false, - }) - }) - it('Should set monthly quota exceeded when throttled for auto file scans', async function () { getFetchStubWithResponse({ status: 200, statusText: 'testing stub' }) await model.CodeScansState.instance.setScansEnabled(true) diff --git a/packages/core/src/test/credentials/provider/ecsCredentialsProvider.test.ts b/packages/core/src/test/credentials/provider/ecsCredentialsProvider.test.ts index 143a987242e..0008e044902 100644 --- a/packages/core/src/test/credentials/provider/ecsCredentialsProvider.test.ts +++ b/packages/core/src/test/credentials/provider/ecsCredentialsProvider.test.ts @@ -4,14 +4,14 @@ */ import assert from 'assert' -import { Credentials } from 'aws-sdk' +import { AwsCredentialIdentity } from '@aws-sdk/types' import { EcsCredentialsProvider } from '../../../auth/providers/ecsCredentialsProvider' import { EnvironmentVariables } from '../../../shared/environmentVariables' describe('EcsCredentialsProvider', function () { const dummyUri = 'dummyUri' const dummyRegion = 'dummmyRegion' - const dummyCredentials = { accessKeyId: 'dummyKey' } as Credentials + const dummyCredentials = { accessKeyId: 'dummyKey' } as AwsCredentialIdentity const dummyProvider = () => { return Promise.resolve(dummyCredentials) } diff --git a/packages/core/src/test/credentials/testUtil.ts b/packages/core/src/test/credentials/testUtil.ts index 629f81b438f..4acbf302a37 100644 --- a/packages/core/src/test/credentials/testUtil.ts +++ b/packages/core/src/test/credentials/testUtil.ts @@ -35,6 +35,7 @@ export const ssoConnection: SsoConnection = { startUrl: 'https://nkomonen.awsapps.com/start', getToken: sinon.stub(), getRegistration: async () => mockRegistration as ClientRegistration, + endpointUrl: undefined, } export const builderIdConnection: SsoConnection = { ...ssoConnection, @@ -46,6 +47,7 @@ export const iamConnection: IamConnection = { id: '0', label: 'iam', getCredentials: sinon.stub(), + endpointUrl: undefined, } export function createSsoProfile(props?: Partial>): SsoProfile { diff --git a/packages/core/src/test/dynamicResources/explorer/resourceTypeNode.test.ts b/packages/core/src/test/dynamicResources/explorer/resourceTypeNode.test.ts index 96130761f0d..86296885df2 100644 --- a/packages/core/src/test/dynamicResources/explorer/resourceTypeNode.test.ts +++ b/packages/core/src/test/dynamicResources/explorer/resourceTypeNode.test.ts @@ -13,7 +13,7 @@ import { assertNodeListOnlyHasPlaceholderNode, } from '../../utilities/explorerNodeAssertions' import { CloudControlClient } from '../../../shared/clients/cloudControl' -import { CloudControl } from 'aws-sdk' +import { ResourceDescription } from '@aws-sdk/client-cloudcontrol' import { ResourceTypeMetadata } from '../../../dynamicResources/model/resources' import sinon from 'sinon' @@ -183,7 +183,7 @@ describe('ResourceTypeNode', function () { cloudControl.listResources = sinon.stub().resolves({ TypeName: fakeTypeName, NextToken: undefined, - ResourceDescriptions: resourceIdentifiers.map((identifier) => { + ResourceDescriptions: resourceIdentifiers.map((identifier) => { return { Identifier: identifier, ResourceModel: '', diff --git a/packages/core/src/test/eventSchemas/commands/downloadSchemaItemCode.test.ts b/packages/core/src/test/eventSchemas/commands/downloadSchemaItemCode.test.ts index 9b630eed07d..140248eaefc 100644 --- a/packages/core/src/test/eventSchemas/commands/downloadSchemaItemCode.test.ts +++ b/packages/core/src/test/eventSchemas/commands/downloadSchemaItemCode.test.ts @@ -7,7 +7,11 @@ import assert from 'assert' import * as path from 'path' import * as vscode from 'vscode' -import { Schemas } from 'aws-sdk' +import { + DescribeCodeBindingResponse, + GetCodeBindingSourceResponse, + PutCodeBindingResponse, +} from '@aws-sdk/client-schemas' import * as sinon from 'sinon' import { @@ -61,8 +65,8 @@ describe('CodeDownloader', function () { describe('codeDownloader', async function () { it('should return an error if the response body is not Buffer', async function () { - const response: Schemas.GetCodeBindingSourceResponse = { - Body: 'Invalied body', + const response: GetCodeBindingSourceResponse = { + Body: 'Invalied body' as any, } sandbox.stub(schemaClient, 'getCodeBindingSource').returns(Promise.resolve(response)) @@ -75,8 +79,8 @@ describe('CodeDownloader', function () { it('should return arrayBuffer for valid Body type', async function () { const myBuffer = Buffer.from('TEST STRING') - const response: Schemas.GetCodeBindingSourceResponse = { - Body: myBuffer, + const response: GetCodeBindingSourceResponse = { + Body: myBuffer as any, } sandbox.stub(schemaClient, 'getCodeBindingSource').returns(Promise.resolve(response)) @@ -148,7 +152,7 @@ describe('CodeGenerator', function () { describe('codeGenerator', async function () { it('should return the current status of code generation', async function () { - const response: Schemas.PutCodeBindingResponse = { + const response: PutCodeBindingResponse = { Status: CodeGenerationStatus.CREATE_IN_PROGRESS, } sandbox.stub(schemaClient, 'putCodeBinding').returns(Promise.resolve(response)) @@ -164,7 +168,7 @@ describe('CodeGenerator', function () { // If code bindings were not generated, but putCodeBinding was already called, ConflictException occurs // Return CREATE_IN_PROGRESS and keep polling in this case it('should return valid code generation status if it gets ConflictException', async function () { - const response: Schemas.PutCodeBindingResponse = { + const response: PutCodeBindingResponse = { Status: CodeGenerationStatus.CREATE_IN_PROGRESS, } @@ -224,10 +228,10 @@ describe('CodeGeneratorStatusPoller', function () { describe('getCurrentStatus', async function () { it('should return the current status of code generation', async function () { - const firstStatus: Schemas.DescribeCodeBindingResponse = { + const firstStatus: DescribeCodeBindingResponse = { Status: CodeGenerationStatus.CREATE_IN_PROGRESS, } - const secondStatus: Schemas.DescribeCodeBindingResponse = { + const secondStatus: DescribeCodeBindingResponse = { Status: CodeGenerationStatus.CREATE_COMPLETE, } @@ -245,7 +249,7 @@ describe('CodeGeneratorStatusPoller', function () { describe('codeGeneratorStatusPoller', async function () { it('fails if code generation status is invalid without retry', async function () { - const schemaResponse: Schemas.DescribeCodeBindingResponse = { + const schemaResponse: DescribeCodeBindingResponse = { Status: CodeGenerationStatus.CREATE_FAILED, } @@ -266,7 +270,7 @@ describe('CodeGeneratorStatusPoller', function () { }) it('times out after max attempts if status is still in progress', async function () { - const schemaResponse: Schemas.DescribeCodeBindingResponse = { + const schemaResponse: DescribeCodeBindingResponse = { Status: CodeGenerationStatus.CREATE_IN_PROGRESS, } @@ -290,7 +294,7 @@ describe('CodeGeneratorStatusPoller', function () { }) it('succeeds when code is previously generated without retry', async function () { - const schemaResponse: Schemas.DescribeCodeBindingResponse = { + const schemaResponse: DescribeCodeBindingResponse = { Status: CodeGenerationStatus.CREATE_COMPLETE, } @@ -402,7 +406,7 @@ describe('SchemaCodeDownload', function () { it('should generate code if download fails with ResourceNotFound and place it into requested directory', async function () { sandbox.stub(poller, 'pollForCompletion').returns(Promise.resolve('CREATE_COMPLETE')) const codeDownloaderStub = sandbox.stub(downloader, 'download') - const codeGeneratorResponse: Schemas.PutCodeBindingResponse = { + const codeGeneratorResponse: PutCodeBindingResponse = { Status: 'CREATE_IN_PROGRESS', } sandbox.stub(generator, 'generate').returns(Promise.resolve(codeGeneratorResponse)) diff --git a/packages/core/src/test/eventSchemas/commands/searchSchemas.test.ts b/packages/core/src/test/eventSchemas/commands/searchSchemas.test.ts index 089a971a40c..dd7c2175ccd 100644 --- a/packages/core/src/test/eventSchemas/commands/searchSchemas.test.ts +++ b/packages/core/src/test/eventSchemas/commands/searchSchemas.test.ts @@ -5,7 +5,7 @@ import assert from 'assert' -import { Schemas } from 'aws-sdk' +import { DescribeSchemaResponse, SearchSchemaSummary, SearchSchemaVersionSummary } from '@aws-sdk/client-schemas' import * as sinon from 'sinon' import { SchemasNode } from '../../../eventSchemas/explorer/schemasNode' import { getTabSizeSetting } from '../../../shared/utilities/editorUtilities' @@ -42,25 +42,25 @@ describe('Search Schemas', function () { const failRegistry = 'failRegistry' const failRegistry2 = 'failRegistry2' - const versionSummary1: Schemas.SearchSchemaVersionSummary = { + const versionSummary1: SearchSchemaVersionSummary = { SchemaVersion: '1', } - const versionSummary2: Schemas.SearchSchemaVersionSummary = { + const versionSummary2: SearchSchemaVersionSummary = { SchemaVersion: '2', } - const searchSummary1: Schemas.SearchSchemaSummary = { + const searchSummary1: SearchSchemaSummary = { RegistryName: testRegistry, SchemaName: 'testSchema1', SchemaVersions: [versionSummary1, versionSummary2], } - const searchSummary2: Schemas.SearchSchemaSummary = { + const searchSummary2: SearchSchemaSummary = { RegistryName: testRegistry, SchemaName: 'testSchema2', SchemaVersions: [versionSummary1], } - const searchSummary3: Schemas.SearchSchemaSummary = { + const searchSummary3: SearchSchemaSummary = { RegistryName: testRegistry2, SchemaName: 'testSchema3', SchemaVersions: [versionSummary1], @@ -178,7 +178,7 @@ describe('Search Schemas', function () { const multipleRegistryNames = [testRegistry, testRegistry2] const awsEventSchemaRaw = '{"openapi":"3.0.0","info":{"version":"1.0.0","title":"Event"},"paths":{},"components":{"schemas":{"Event":{"type":"object"}}}}' - const schemaResponse: Schemas.DescribeSchemaResponse = { + const schemaResponse: DescribeSchemaResponse = { Content: awsEventSchemaRaw, } diff --git a/packages/core/src/test/eventSchemas/commands/viewSchemaItem.test.ts b/packages/core/src/test/eventSchemas/commands/viewSchemaItem.test.ts index c7e063dbec2..9127bfe96a5 100644 --- a/packages/core/src/test/eventSchemas/commands/viewSchemaItem.test.ts +++ b/packages/core/src/test/eventSchemas/commands/viewSchemaItem.test.ts @@ -3,8 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Schemas } from 'aws-sdk' - +import { DescribeSchemaResponse } from '@aws-sdk/client-schemas' import assert from 'assert' import * as sinon from 'sinon' import * as vscode from 'vscode' @@ -117,7 +116,7 @@ describe('viewSchemaItem', async function () { } function generateSchemaItemNode(): SchemaItemNode { - const schemaResponse: Schemas.DescribeSchemaResponse = { + const schemaResponse: DescribeSchemaResponse = { Content: awsEventSchemaRaw, } const schemaClient = new DefaultSchemaClient('') diff --git a/packages/core/src/test/eventSchemas/explorer/registryItemNode.test.ts b/packages/core/src/test/eventSchemas/explorer/registryItemNode.test.ts index 5fdd61e5c42..719520cde64 100644 --- a/packages/core/src/test/eventSchemas/explorer/registryItemNode.test.ts +++ b/packages/core/src/test/eventSchemas/explorer/registryItemNode.test.ts @@ -6,7 +6,7 @@ import assert from 'assert' import * as os from 'os' import * as sinon from 'sinon' -import { Schemas } from 'aws-sdk' +import { RegistrySummary, SchemaSummary } from '@aws-sdk/client-schemas' import { RegistryItemNode } from '../../../eventSchemas/explorer/registryItemNode' import { SchemaItemNode } from '../../../eventSchemas/explorer/schemaItemNode' import { SchemasNode } from '../../../eventSchemas/explorer/schemasNode' @@ -21,7 +21,7 @@ import { asyncGenerator } from '../../../shared/utilities/collectionUtils' import { getIcon } from '../../../shared/icons' import { stub } from '../../utilities/stubber' -function createSchemaClient(data?: { schemas?: Schemas.SchemaSummary[]; registries?: Schemas.RegistrySummary[] }) { +function createSchemaClient(data?: { schemas?: SchemaSummary[]; registries?: RegistrySummary[] }) { const client = stub(DefaultSchemaClient, { regionCode: 'code' }) client.listSchemas.callsFake(() => asyncGenerator(data?.schemas ?? [])) client.listRegistries.callsFake(() => asyncGenerator(data?.registries ?? [])) @@ -30,7 +30,7 @@ function createSchemaClient(data?: { schemas?: Schemas.SchemaSummary[]; registri } describe('RegistryItemNode', function () { - let fakeRegistry: Schemas.RegistrySummary + let fakeRegistry: RegistrySummary before(function () { fakeRegistry = { @@ -58,17 +58,17 @@ describe('RegistryItemNode', function () { }) it('returns schemas that belong to Registry', async function () { - const schema1Item: Schemas.SchemaSummary = { + const schema1Item: SchemaSummary = { SchemaArn: 'arn:schema1', SchemaName: 'schema1Name', } - const schema2Item: Schemas.SchemaSummary = { + const schema2Item: SchemaSummary = { SchemaArn: 'arn:schema1', SchemaName: 'schema2Name', } - const schema3Item: Schemas.SchemaSummary = { + const schema3Item: SchemaSummary = { SchemaArn: 'arn:schema1', SchemaName: 'schema3Name', } diff --git a/packages/core/src/test/eventSchemas/model/schemaCodeLangs.test.ts b/packages/core/src/test/eventSchemas/model/schemaCodeLangs.test.ts index 619f7bd035e..cce239a1cb1 100644 --- a/packages/core/src/test/eventSchemas/model/schemaCodeLangs.test.ts +++ b/packages/core/src/test/eventSchemas/model/schemaCodeLangs.test.ts @@ -10,6 +10,7 @@ import { schemaCodeLangs, } from '../../../eventSchemas/models/schemaCodeLangs' import { samZipLambdaRuntimes } from '../../../lambda/models/samLambdaRuntime' +import { Runtime } from '@aws-sdk/client-lambda' describe('getLanguageDetails', function () { it('should successfully return details for supported languages', function () { @@ -32,7 +33,8 @@ describe('getApiValueForSchemasDownload', function () { case 'python3.9': case 'python3.11': case 'python3.12': - case 'python3.13': + case 'python3.13' as Runtime: + case 'python3.14' as Runtime: case 'python3.10': { const result = getApiValueForSchemasDownload(runtime) assert.strictEqual(result, 'Python36', 'Api value used by schemas api') diff --git a/packages/core/src/test/lambda/activation.test.ts b/packages/core/src/test/lambda/activation.test.ts new file mode 100644 index 00000000000..89c647e5f0c --- /dev/null +++ b/packages/core/src/test/lambda/activation.test.ts @@ -0,0 +1,237 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { LambdaFunctionNode } from '../../lambda/explorer/lambdaFunctionNode' +import * as treeNodeUtils from '../../shared/utilities/treeNodeUtils' +import * as resourceNode from '../../awsService/appBuilder/explorer/nodes/resourceNode' +import * as invokeLambdaModule from '../../lambda/vue/remoteInvoke/invokeLambda' +import * as tailLogGroupModule from '../../awsService/cloudWatchLogs/commands/tailLogGroup' +import { LogDataRegistry } from '../../awsService/cloudWatchLogs/registry/logDataRegistry' +import * as searchLogGroupModule from '../../awsService/cloudWatchLogs/commands/searchLogGroup' + +const mockGeneratedLambdaNode: LambdaFunctionNode = { + functionName: 'generatedFunction', + regionCode: 'us-east-1', + configuration: { + FunctionName: 'generatedFunction', + FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:generatedFunction', + }, +} as LambdaFunctionNode + +const mockTreeNode = { + resource: { + deployedResource: { LogicalResourceId: 'TestFunction' }, + region: 'us-east-1', + stackName: 'TestStack', + resource: { Id: 'TestFunction', Type: 'AWS::Serverless::Function' }, + }, +} + +const mockLambdaNode: LambdaFunctionNode = { + functionName: 'testFunction', + regionCode: 'us-west-2', + configuration: { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + LoggingConfig: { + LogGroup: '/aws/lambda/custom-log-group', + }, + }, +} as LambdaFunctionNode + +describe('Lambda activation', () => { + let sandbox: sinon.SinonSandbox + let getSourceNodeStub: sinon.SinonStub + let generateLambdaNodeFromResourceStub: sinon.SinonStub + let invokeRemoteLambdaStub: sinon.SinonStub + let tailLogGroupStub: sinon.SinonStub + let isTreeNodeStub: sinon.SinonStub + let searchLogGroupStub: sinon.SinonStub + let registry: LogDataRegistry + + beforeEach(async () => { + sandbox = sinon.createSandbox() + searchLogGroupStub = sandbox.stub(searchLogGroupModule, 'searchLogGroup') + registry = LogDataRegistry.instance + getSourceNodeStub = sandbox.stub(treeNodeUtils, 'getSourceNode') + generateLambdaNodeFromResourceStub = sandbox.stub(resourceNode, 'generateLambdaNodeFromResource') + invokeRemoteLambdaStub = sandbox.stub(invokeLambdaModule, 'invokeRemoteLambda') + tailLogGroupStub = sandbox.stub(tailLogGroupModule, 'tailLogGroup') + isTreeNodeStub = sandbox.stub(require('../../shared/treeview/resourceTreeDataProvider'), 'isTreeNode') + }) + + afterEach(() => { + sandbox.restore() + }) + describe('aws.appBuilder.searchLogs command', () => { + it('should handle LambdaFunctionNode directly', async () => { + getSourceNodeStub.returns(mockLambdaNode) + isTreeNodeStub.returns(false) + searchLogGroupStub.resolves() + + const node = {} + await vscode.commands.executeCommand('aws.appBuilder.searchLogs', node) + + assert(searchLogGroupStub.calledOnce) + assert( + searchLogGroupStub.calledWith(registry, 'AppBuilderSearchLogs', { + regionName: 'us-west-2', + groupName: '/aws/lambda/custom-log-group', + }) + ) + }) + + it('should generate LambdaFunctionNode from TreeNode when getSourceNode returns undefined', async () => { + getSourceNodeStub.returns(undefined) + isTreeNodeStub.returns(true) + generateLambdaNodeFromResourceStub.resolves(mockGeneratedLambdaNode) + searchLogGroupStub.resolves() + + await vscode.commands.executeCommand('aws.appBuilder.searchLogs', mockTreeNode) + + assert(generateLambdaNodeFromResourceStub.calledOnce) + assert(generateLambdaNodeFromResourceStub.calledWith(mockTreeNode.resource)) + assert(searchLogGroupStub.calledOnce) + assert( + searchLogGroupStub.calledWith(registry, 'AppBuilderSearchLogs', { + regionName: 'us-east-1', + groupName: '/aws/lambda/generatedFunction', + }) + ) + }) + + it('should log error and throw ToolkitError when generateLambdaNodeFromResource fails', async () => { + getSourceNodeStub.returns(undefined) + isTreeNodeStub.returns(true) + generateLambdaNodeFromResourceStub.rejects(new Error('Failed to generate node')) + searchLogGroupStub.resolves() + + await vscode.commands.executeCommand('aws.appBuilder.searchLogs', mockTreeNode) + assert(searchLogGroupStub.notCalled) + }) + }) + + describe('aws.invokeLambda command', () => { + it('should handle LambdaFunctionNode directly from AWS Explorer', async () => { + isTreeNodeStub.returns(false) + invokeRemoteLambdaStub.resolves() + + await vscode.commands.executeCommand('aws.invokeLambda', mockLambdaNode) + + assert(invokeRemoteLambdaStub.calledOnce) + const callArgs = invokeRemoteLambdaStub.getCall(0).args + assert.strictEqual(callArgs[1].source, 'AwsExplorerRemoteInvoke') + assert.strictEqual(callArgs[1].functionNode, mockLambdaNode) + }) + + it('should generate LambdaFunctionNode from TreeNode when coming from AppBuilder', async () => { + isTreeNodeStub.returns(true) + getSourceNodeStub.returns(undefined) + generateLambdaNodeFromResourceStub.resolves(mockGeneratedLambdaNode) + invokeRemoteLambdaStub.resolves() + + await vscode.commands.executeCommand('aws.invokeLambda', mockTreeNode) + + assert(generateLambdaNodeFromResourceStub.calledOnce) + assert(generateLambdaNodeFromResourceStub.calledWith(mockTreeNode.resource)) + assert(invokeRemoteLambdaStub.calledOnce) + const callArgs = invokeRemoteLambdaStub.getCall(0).args + assert.strictEqual(callArgs[1].source, 'AppBuilderRemoteInvoke') + assert.strictEqual(callArgs[1].functionNode, mockGeneratedLambdaNode) + }) + + it('should handle existing LambdaFunctionNode from TreeNode', async () => { + const mockTreeNode = { + resource: {}, + } + + isTreeNodeStub.returns(true) + getSourceNodeStub.returns(mockLambdaNode) + invokeRemoteLambdaStub.resolves() + + await vscode.commands.executeCommand('aws.invokeLambda', mockTreeNode) + + assert(generateLambdaNodeFromResourceStub.notCalled) + assert(invokeRemoteLambdaStub.calledOnce) + const callArgs = invokeRemoteLambdaStub.getCall(0).args + assert.strictEqual(callArgs[1].source, 'AppBuilderRemoteInvoke') + assert.strictEqual(callArgs[1].functionNode, mockLambdaNode) + }) + }) + + describe('aws.appBuilder.tailLogs command', () => { + it('should handle LambdaFunctionNode directly', async () => { + isTreeNodeStub.returns(false) + getSourceNodeStub.returns(mockLambdaNode) + tailLogGroupStub.resolves() + + await vscode.commands.executeCommand('aws.appBuilder.tailLogs', mockLambdaNode) + + assert(tailLogGroupStub.calledOnce) + const callArgs = tailLogGroupStub.getCall(0).args + assert.strictEqual(callArgs[1], 'AwsExplorerLambdaNode') + assert.deepStrictEqual(callArgs[3], { + regionName: 'us-west-2', + groupName: '/aws/lambda/custom-log-group', + }) + assert.deepStrictEqual(callArgs[4], { type: 'all' }) + }) + + it('should generate LambdaFunctionNode from TreeNode when getSourceNode returns undefined', async () => { + const mockGeneratedLambdaNode: LambdaFunctionNode = { + functionName: 'generatedFunction', + regionCode: 'us-east-1', + configuration: { + FunctionName: 'generatedFunction', + }, + } as LambdaFunctionNode + + isTreeNodeStub.returns(true) + getSourceNodeStub.returns(undefined) + generateLambdaNodeFromResourceStub.resolves(mockGeneratedLambdaNode) + tailLogGroupStub.resolves() + + await vscode.commands.executeCommand('aws.appBuilder.tailLogs', mockTreeNode) + + assert(generateLambdaNodeFromResourceStub.calledOnce) + assert(generateLambdaNodeFromResourceStub.calledWith(mockTreeNode.resource)) + assert(tailLogGroupStub.calledOnce) + const callArgs = tailLogGroupStub.getCall(0).args + assert.strictEqual(callArgs[1], 'AppBuilder') + assert.deepStrictEqual(callArgs[3], { + regionName: 'us-east-1', + groupName: '/aws/lambda/generatedFunction', + }) + assert.deepStrictEqual(callArgs[4], { type: 'all' }) + }) + + it('should use correct source for TreeNode', async () => { + const mockLambdaNode: LambdaFunctionNode = { + functionName: 'testFunction', + regionCode: 'us-west-2', + configuration: { + FunctionName: 'testFunction', + }, + } as LambdaFunctionNode + + const mockTreeNode = { + resource: {}, + } + + isTreeNodeStub.returns(true) + getSourceNodeStub.returns(mockLambdaNode) + tailLogGroupStub.resolves() + + await vscode.commands.executeCommand('aws.appBuilder.tailLogs', mockTreeNode) + + assert(tailLogGroupStub.calledOnce) + const callArgs = tailLogGroupStub.getCall(0).args + assert.strictEqual(callArgs[1], 'AppBuilder') + }) + }) +}) diff --git a/packages/core/src/test/lambda/commands/copyLambdaUrl.test.ts b/packages/core/src/test/lambda/commands/copyLambdaUrl.test.ts index e55267182a3..acd3b98f052 100644 --- a/packages/core/src/test/lambda/commands/copyLambdaUrl.test.ts +++ b/packages/core/src/test/lambda/commands/copyLambdaUrl.test.ts @@ -10,7 +10,7 @@ import { LambdaFunctionNode } from '../../../lambda/explorer/lambdaFunctionNode' import { DefaultLambdaClient, LambdaClient } from '../../../shared/clients/lambdaClient' import { addCodiconToString } from '../../../shared/utilities/textUtilities' import { env } from 'vscode' -import { FunctionUrlConfig } from 'aws-sdk/clients/lambda' +import { FunctionUrlConfig } from '@aws-sdk/client-lambda' import { createQuickPickPrompterTester } from '../../shared/ui/testUtils' import { getTestWindow } from '../../shared/vscode/window' @@ -22,11 +22,11 @@ import { getTestWindow } from '../../shared/vscode/window' */ export function buildFunctionUrlConfig(options: Partial): FunctionUrlConfig { return { - AuthType: options.AuthType ?? '', - CreationTime: options.CreationTime ?? '', - FunctionArn: options.FunctionArn ?? '', - FunctionUrl: options.FunctionUrl ?? '', - LastModifiedTime: options.LastModifiedTime ?? '', + AuthType: options.AuthType, + CreationTime: options.CreationTime, + FunctionArn: options.FunctionArn, + FunctionUrl: options.FunctionUrl, + LastModifiedTime: options.LastModifiedTime, } } @@ -94,10 +94,10 @@ describe('lambda func url prompter', async () => { const tester = createQuickPickPrompterTester(prompter) tester.assertItems( configList.map((c) => { - return { label: c.FunctionArn, data: c.FunctionUrl } // order matters + return { label: c.FunctionArn!, data: c.FunctionUrl } // order matters }) ) - tester.acceptItem(configList[1].FunctionArn) + tester.acceptItem(configList[1].FunctionArn!) await tester.result(configList[1].FunctionUrl) }) }) diff --git a/packages/core/src/test/lambda/commands/createNewSamApp.test.ts b/packages/core/src/test/lambda/commands/createNewSamApp.test.ts index 8bb25119301..c31f922483c 100644 --- a/packages/core/src/test/lambda/commands/createNewSamApp.test.ts +++ b/packages/core/src/test/lambda/commands/createNewSamApp.test.ts @@ -26,7 +26,7 @@ import { import { normalize } from '../../../shared/utilities/pathUtils' import { getIdeProperties, isCloud9 } from '../../../shared/extensionUtilities' import globals from '../../../shared/extensionGlobals' -import { Runtime } from '../../../shared/telemetry/telemetry' +import { Runtime } from '@aws-sdk/client-lambda' import { stub } from '../../utilities/stubber' import sinon from 'sinon' import { fs } from '../../../shared' diff --git a/packages/core/src/test/lambda/explorer/cloudFormationNodes.test.ts b/packages/core/src/test/lambda/explorer/cloudFormationNodes.test.ts index ba8d7ccd516..2100b1ae4a5 100644 --- a/packages/core/src/test/lambda/explorer/cloudFormationNodes.test.ts +++ b/packages/core/src/test/lambda/explorer/cloudFormationNodes.test.ts @@ -22,6 +22,7 @@ import { } from '../../utilities/explorerNodeAssertions' import { stub } from '../../utilities/stubber' import { getLabel } from '../../../shared/treeview/utils' +import { AWSCommandTreeNode } from '../../../shared/treeview/nodes/awsCommandTreeNode' const regionCode = 'someregioncode' @@ -168,8 +169,15 @@ describe('CloudFormationNode', function () { const cloudFormationNode = new CloudFormationNode(regionCode, client) const children = await cloudFormationNode.getChildren() - for (const node of children) { - assert.ok(node instanceof CloudFormationStackNode, 'Expected child node to be CloudFormationStackNode') + // First node should be the panel promotion node + assert.ok(children[0] instanceof AWSCommandTreeNode, 'Expected first child to be panel promotion node') + + // Remaining nodes should be CloudFormationStackNode + for (let i = 1; i < children.length; i++) { + assert.ok( + children[i] instanceof CloudFormationStackNode, + 'Expected child node to be CloudFormationStackNode' + ) } }) @@ -178,16 +186,19 @@ describe('CloudFormationNode', function () { const cloudFormationNode = new CloudFormationNode(regionCode, client) const children = await cloudFormationNode.getChildren() - const actualChildOrder = children.map((node) => (node as CloudFormationStackNode).stackName) + // Skip the first node (panel promotion) and check stack sorting + const stackNodes = children.slice(1) as CloudFormationStackNode[] + const actualChildOrder = stackNodes.map((node) => node.stackName) assert.deepStrictEqual(actualChildOrder, ['a', 'b'], 'Unexpected child sort order') }) - it('returns placeholder node if no children are present', async function () { + it('returns panel promotion node if no stacks are present', async function () { const client = createCloudFormationClient() const cloudFormationNode = new CloudFormationNode(regionCode, client) const children = await cloudFormationNode.getChildren() - assertNodeListOnlyHasPlaceholderNode(children) + assert.strictEqual(children.length, 1, 'Expected exactly one child node') + assert.ok(children[0] instanceof AWSCommandTreeNode, 'Expected panel promotion node') }) it('has an error node for a child if an error happens during loading', async function () { diff --git a/packages/core/src/test/lambda/explorer/lambdaCapacityProviderNode.test.ts b/packages/core/src/test/lambda/explorer/lambdaCapacityProviderNode.test.ts new file mode 100644 index 00000000000..d1b72598cd1 --- /dev/null +++ b/packages/core/src/test/lambda/explorer/lambdaCapacityProviderNode.test.ts @@ -0,0 +1,27 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { LambdaCapacityProviderNode } from '../../../lambda/explorer/lambdaCapacityProviderNode' +import { contextValueLambdaCapacityProvider } from '../../../lambda/explorer/lambdaCapacityProviderNode' + +describe('LambdaCapacityProviderNode', function () { + it('instantiates without issue', async function () { + const fakeCapacityProviderResource = { + LogicalResourceId: 'testLogicalResourceId', + PhysicalResourceId: 'testPhysicalResourceId', + } + + const testNode = new LambdaCapacityProviderNode( + 'someregioncode', + fakeCapacityProviderResource, + contextValueLambdaCapacityProvider + ) + assert.ok(testNode) + assert.strictEqual(testNode.regionCode, 'someregioncode') + assert.strictEqual(testNode.label, fakeCapacityProviderResource.LogicalResourceId) + assert.strictEqual(testNode.name, fakeCapacityProviderResource.LogicalResourceId) + }) +}) diff --git a/packages/core/src/test/lambda/local/debugConfiguration.test.ts b/packages/core/src/test/lambda/local/debugConfiguration.test.ts index 12192127eeb..0a0aab0dc99 100644 --- a/packages/core/src/test/lambda/local/debugConfiguration.test.ts +++ b/packages/core/src/test/lambda/local/debugConfiguration.test.ts @@ -18,7 +18,7 @@ import { CloudFormationTemplateRegistry } from '../../../shared/fs/templateRegis import { getArchitecture, isImageLambdaConfig } from '../../../lambda/local/debugConfiguration' import * as CloudFormation from '../../../shared/cloudformation/cloudformation' import globals from '../../../shared/extensionGlobals' -import { Runtime } from '../../../shared/telemetry/telemetry' +import { Runtime } from '@aws-sdk/client-lambda' import { fs } from '../../../shared' describe('makeCoreCLRDebugConfiguration', function () { diff --git a/packages/core/src/test/lambda/models/samLambdaRuntime.test.ts b/packages/core/src/test/lambda/models/samLambdaRuntime.test.ts index f47cc3fe06b..dc872530415 100644 --- a/packages/core/src/test/lambda/models/samLambdaRuntime.test.ts +++ b/packages/core/src/test/lambda/models/samLambdaRuntime.test.ts @@ -4,7 +4,6 @@ */ import assert from 'assert' -import { Runtime } from 'aws-sdk/clients/lambda' import { compareSamLambdaRuntime, getDependencyManager, @@ -16,11 +15,12 @@ import { getNodeMajorVersion, nodeJsRuntimes, } from '../../../lambda/models/samLambdaRuntime' +import { Runtime } from '@aws-sdk/client-lambda' describe('compareSamLambdaRuntime', async function () { const scenarios: { - lowerRuntime: Runtime - higherRuntime: Runtime + lowerRuntime: string + higherRuntime: string }[] = [ { lowerRuntime: 'nodejs14.x', higherRuntime: 'nodejs16.x' }, { lowerRuntime: 'nodejs16.x', higherRuntime: 'nodejs16.x (Image)' }, @@ -48,13 +48,13 @@ describe('getDependencyManager', function () { assert.throws(() => getDependencyManager('nodejs')) }) it('throws on unknown runtimes', function () { - assert.throws(() => getDependencyManager('BASIC')) + assert.throws(() => getDependencyManager('BASIC' as Runtime)) }) }) describe('getFamily', function () { it('unknown runtime name', function () { - assert.strictEqual(getFamily('foo'), RuntimeFamily.Unknown) + assert.strictEqual(getFamily('foo' as Runtime), RuntimeFamily.Unknown) }) it('handles all known runtimes', function () { for (const runtime of samZipLambdaRuntimes) { @@ -74,10 +74,12 @@ describe('runtimes', function () { 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', + 'nodejs24.x', 'python3.10', 'python3.11', 'python3.12', 'python3.13', + 'python3.14', 'python3.7', 'python3.8', 'python3.9', @@ -88,10 +90,12 @@ describe('runtimes', function () { 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', + 'nodejs24.x', 'python3.10', 'python3.11', 'python3.12', 'python3.13', + 'python3.14', 'python3.7', 'python3.8', 'python3.9', @@ -105,6 +109,7 @@ describe('runtimes', function () { 'java11', 'java17', 'java21', + 'java25', 'java8', 'java8.al2', 'nodejs14.x', @@ -112,10 +117,12 @@ describe('runtimes', function () { 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', + 'nodejs24.x', 'python3.10', 'python3.11', 'python3.12', 'python3.13', + 'python3.14', 'python3.7', 'python3.8', 'python3.9', @@ -128,6 +135,7 @@ describe('runtimes', function () { 'java11', 'java17', 'java21', + 'java25', 'java8', 'java8.al2', 'nodejs14.x', @@ -135,10 +143,12 @@ describe('runtimes', function () { 'nodejs18.x', 'nodejs20.x', 'nodejs22.x', + 'nodejs24.x', 'python3.10', 'python3.11', 'python3.12', 'python3.13', + 'python3.14', 'python3.7', 'python3.8', 'python3.9', diff --git a/packages/core/src/test/lambda/models/samTemplates.test.ts b/packages/core/src/test/lambda/models/samTemplates.test.ts index 4abb3f87315..32d2f3caa7b 100644 --- a/packages/core/src/test/lambda/models/samTemplates.test.ts +++ b/packages/core/src/test/lambda/models/samTemplates.test.ts @@ -20,6 +20,7 @@ import { import { Set } from 'immutable' import { samZipLambdaRuntimes } from '../../../lambda/models/samLambdaRuntime' +import { Runtime } from '@aws-sdk/client-lambda' let validTemplateOptions: Set let validPythonTemplateOptions: Set @@ -66,7 +67,8 @@ describe('getSamTemplateWizardOption', function () { case 'python3.10': case 'python3.11': case 'python3.12': - case 'python3.13': + case 'python3.13' as Runtime: + case 'python3.14' as Runtime: assert.deepStrictEqual( result, validPythonTemplateOptions, diff --git a/packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts b/packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts index 91f99aa0409..eb4678da2f0 100644 --- a/packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts +++ b/packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts @@ -5,19 +5,136 @@ import assert from 'assert' import sinon from 'sinon' -import { Lambda } from 'aws-sdk' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' +import type { UserAgent } from '@aws-sdk/types' import { LdkClient, getRegionFromArn, isTunnelInfo } from '../../../lambda/remoteDebugging/ldkClient' import { LocalProxy } from '../../../lambda/remoteDebugging/localProxy' import * as utils from '../../../lambda/remoteDebugging/utils' import * as telemetryUtil from '../../../shared/telemetry/util' import globals from '../../../shared/extensionGlobals' import { createMockFunctionConfig, createMockProgress } from './testUtils' +import { + IoTSecureTunnelingClient, + IoTSecureTunnelingClientResolvedConfig, + ListTunnelsCommand, + OpenTunnelCommand, + RotateTunnelAccessTokenCommand, + ServiceInputTypes, + ServiceOutputTypes, + TunnelStatus, +} from '@aws-sdk/client-iotsecuretunneling' +import { AwsStub, mockClient } from 'aws-sdk-client-mock' +import * as http from 'http' +import { AWSClientBuilderV3 } from '../../../shared/awsClientBuilderV3' +import { FakeAwsContext } from '../../utilities/fakeAwsContext' +import { Any } from '../../../shared/utilities/typeConstructors' + +describe('Remote Debugging User-Agent test', () => { + let sandbox: sinon.SinonSandbox + let ldkClient: LdkClient + let mockServer: http.Server + let capturedHeaders: http.IncomingHttpHeaders | undefined + let sdkBuilderTmp: Any + let mockLocalProxy: any + + before(async () => { + sdkBuilderTmp = globals.sdkClientBuilderV3 + + mockServer = http.createServer((req, res) => { + capturedHeaders = req.headers + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end() + }) + + // Start the mock server + await new Promise((resolve) => { + mockServer.listen(0, '127.0.0.1', () => { + resolve() + }) + }) + + const port = (mockServer.address() as any).port + globals.sdkClientBuilderV3 = new AWSClientBuilderV3( + new FakeAwsContext({ + contextCredentials: { + endpointUrl: `http://127.0.0.1:${port}`, + credentials: undefined as any, + credentialsId: '', + }, + }) + ) + }) + + beforeEach(() => { + sandbox = sinon.createSandbox() + sandbox.stub(telemetryUtil, 'getClientId').returns('test-client-id') + capturedHeaders = undefined + // Mock LocalProxy + mockLocalProxy = { + start: sandbox.stub(), + stop: sandbox.stub(), + } + sandbox.stub(LocalProxy.prototype, 'start').callsFake(mockLocalProxy.start) + sandbox.stub(LocalProxy.prototype, 'stop').callsFake(mockLocalProxy.stop) + ldkClient = LdkClient.instance + ;(ldkClient as any).localProxy = mockLocalProxy + ldkClient.dispose() + }) + + afterEach(() => { + sandbox.restore() + }) + + after(async () => { + globals.sdkClientBuilderV3 = sdkBuilderTmp + // Close the server + mockServer.close() + }) + + for (const scenario of ['Lambda', 'IoT']) { + it(`should send ${scenario} request with correct User-Agent header to mock server`, async () => { + try { + switch (scenario) { + case 'Lambda': + await ldkClient.getFunctionDetail('arn:aws:lambda:us-east-1:123456789012:function:testFunction') + break + case 'IoT': + await ldkClient.createOrReuseTunnel('us-east-1') + break + } + } catch (e) { + // Ignore errors from the mock response, we just want to capture headers + } + + // Verify the User-Agent header was sent correctly + assert(capturedHeaders, 'Should have captured request headers') + const userAgent = capturedHeaders!['user-agent'] || capturedHeaders!['User-Agent'] + assert(userAgent, 'Should have User-Agent header') + + // The User-Agent should contain our custom user agent pairs + assert( + userAgent.includes('LAMBDA-DEBUG/1.0.0'), + `User-Agent should include LAMBDA-DEBUG/1.0.0, got: ${userAgent}` + ) + // Check for presence of other user agent components without checking specific values + assert( + userAgent.includes('AWS-Toolkit-For-VSCode/'), + `User-Agent should include AWS-Toolkit-For-VSCode/, got: ${userAgent}` + ) + assert( + userAgent.includes('Visual-Studio-Code'), + `User-Agent should include Visual-Studio-Code, got: ${userAgent}` + ) + assert(userAgent.includes('ClientId/'), `User-Agent should include ClientId/, got: ${userAgent}`) + }) + } +}) describe('LdkClient', () => { let sandbox: sinon.SinonSandbox let ldkClient: LdkClient let mockLambdaClient: any - let mockIoTSTClient: any + let mockIoTSTClient: AwsStub let mockLocalProxy: any beforeEach(() => { @@ -32,15 +149,8 @@ describe('LdkClient', () => { } sandbox.stub(utils, 'getLambdaClientWithAgent').returns(mockLambdaClient) - // Mock IoT ST client with proper promise structure - const createPromiseStub = () => sandbox.stub() - mockIoTSTClient = { - listTunnels: sandbox.stub().returns({ promise: createPromiseStub() }), - openTunnel: sandbox.stub().returns({ promise: createPromiseStub() }), - closeTunnel: sandbox.stub().returns({ promise: createPromiseStub() }), - rotateTunnelAccessToken: sandbox.stub().returns({ promise: createPromiseStub() }), - } - sandbox.stub(utils, 'getIoTSTClientWithAgent').resolves(mockIoTSTClient) + mockIoTSTClient = mockClient(IoTSecureTunnelingClient) + sandbox.stub(utils, 'getIoTSTClientWithAgent').returns(mockIoTSTClient as any) // Mock LocalProxy mockLocalProxy = { @@ -105,8 +215,8 @@ describe('LdkClient', () => { describe('createOrReuseTunnel()', () => { it('should create new tunnel when none exists', async () => { - mockIoTSTClient.listTunnels().promise.resolves({ tunnelSummaries: [] }) - mockIoTSTClient.openTunnel().promise.resolves({ + mockIoTSTClient.on(ListTunnelsCommand).resolves({ tunnelSummaries: [] }) + mockIoTSTClient.on(OpenTunnelCommand).resolves({ tunnelId: 'tunnel-123', sourceAccessToken: 'source-token', destinationAccessToken: 'dest-token', @@ -118,20 +228,24 @@ describe('LdkClient', () => { assert.strictEqual(result?.tunnelID, 'tunnel-123') assert.strictEqual(result?.sourceToken, 'source-token') assert.strictEqual(result?.destinationToken, 'dest-token') - assert(mockIoTSTClient.listTunnels.called, 'Should list existing tunnels') - assert(mockIoTSTClient.openTunnel.called, 'Should create new tunnel') + assert.strictEqual( + mockIoTSTClient.commandCalls(ListTunnelsCommand).length, + 1, + 'Should list existing tunnels' + ) + assert.strictEqual(mockIoTSTClient.commandCalls(OpenTunnelCommand).length, 1, 'Should create new tunnel') }) it('should reuse existing tunnel with sufficient time remaining', async () => { const existingTunnel = { tunnelId: 'existing-tunnel', description: 'RemoteDebugging+test-client-id', - status: 'OPEN', + status: TunnelStatus.OPEN, createdAt: new Date(Date.now() - 60 * 60 * 1000), // 1 hour ago } - mockIoTSTClient.listTunnels().promise.resolves({ tunnelSummaries: [existingTunnel] }) - mockIoTSTClient.rotateTunnelAccessToken().promise.resolves({ + mockIoTSTClient.on(ListTunnelsCommand).resolves({ tunnelSummaries: [existingTunnel] }) + mockIoTSTClient.on(RotateTunnelAccessTokenCommand).resolves({ sourceAccessToken: 'rotated-source-token', destinationAccessToken: 'rotated-dest-token', }) @@ -145,8 +259,8 @@ describe('LdkClient', () => { }) it('should handle tunnel creation errors', async () => { - mockIoTSTClient.listTunnels().promise.resolves({ tunnelSummaries: [] }) - mockIoTSTClient.openTunnel().promise.rejects(new Error('Tunnel creation failed')) + mockIoTSTClient.on(ListTunnelsCommand).resolves({ tunnelSummaries: [] }) + mockIoTSTClient.on(OpenTunnelCommand).rejects(new Error('Tunnel creation failed')) await assert.rejects( async () => await ldkClient.createOrReuseTunnel('us-east-1'), @@ -158,7 +272,7 @@ describe('LdkClient', () => { describe('refreshTunnelTokens()', () => { it('should refresh tunnel tokens successfully', async () => { - mockIoTSTClient.rotateTunnelAccessToken().promise.resolves({ + mockIoTSTClient.on(RotateTunnelAccessTokenCommand).resolves({ sourceAccessToken: 'new-source-token', destinationAccessToken: 'new-dest-token', }) @@ -172,7 +286,7 @@ describe('LdkClient', () => { }) it('should handle token refresh errors', async () => { - mockIoTSTClient.rotateTunnelAccessToken().promise.rejects(new Error('Token refresh failed')) + mockIoTSTClient.on(RotateTunnelAccessTokenCommand).rejects(new Error('Token refresh failed')) await assert.rejects( async () => await ldkClient.refreshTunnelTokens('tunnel-123', 'us-east-1'), @@ -183,7 +297,7 @@ describe('LdkClient', () => { }) describe('getFunctionDetail()', () => { - const mockFunctionConfig: Lambda.FunctionConfiguration = createMockFunctionConfig({ + const mockFunctionConfig: FunctionConfiguration = createMockFunctionConfig({ FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:testFunction', }) @@ -212,7 +326,7 @@ describe('LdkClient', () => { }) describe('createDebugDeployment()', () => { - const mockFunctionConfig: Lambda.FunctionConfiguration = createMockFunctionConfig({ + const mockFunctionConfig: FunctionConfiguration = createMockFunctionConfig({ FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:testFunction', }) @@ -291,7 +405,7 @@ describe('LdkClient', () => { }) describe('removeDebugDeployment()', () => { - const mockFunctionConfig: Lambda.FunctionConfiguration = createMockFunctionConfig({ + const mockFunctionConfig: FunctionConfiguration = createMockFunctionConfig({ FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:testFunction', }) @@ -415,6 +529,103 @@ describe('LdkClient', () => { assert.strictEqual(result, true, 'Should return true when no proxy to stop') }) }) + + describe('Client User-Agent', () => { + let expectedUserAgentPairs: UserAgent + let userAgentSandbox: sinon.SinonSandbox + beforeEach(() => { + userAgentSandbox = sinon.createSandbox() + // Stub getUserAgentPairs at the telemetryUtil level to return known pairs + const getUserAgentPairsStub = userAgentSandbox.stub(telemetryUtil, 'getUserAgentPairs') + const generalUserAgents: UserAgent = [ + ['AWS-Toolkit-For-VSCode', 'testVersionForUA'], + ['Visual-Studio-Code', '1.0.0'], + ['ClientId', 'test-client-id'], + ] + getUserAgentPairsStub.returns(generalUserAgents) + const lambdaDebugUserAgent: UserAgent = [['LAMBDA-DEBUG', '1.0.0']] + expectedUserAgentPairs = lambdaDebugUserAgent.concat(generalUserAgents) + }) + + afterEach(() => { + userAgentSandbox.restore() + }) + + it('should create Lambda client with correct user-agent', async () => { + // Restore the existing stub and create a new one to track calls + const existingStub = (utils.getLambdaClientWithAgent as any).restore + ? (utils.getLambdaClientWithAgent as sinon.SinonStub) + : undefined + if (existingStub) { + existingStub.restore() + } + + // Stub the Lambda sdkClientBuilderV3 to capture the client options + let capturedClientOptions: any + const createAwsServiceStubLambda = userAgentSandbox.stub(globals.sdkClientBuilderV3, 'createAwsService') + createAwsServiceStubLambda.callsFake((options: any) => { + capturedClientOptions = options + // Return a mock Lambda client that has the required methods + return { + send: async () => ({ + Configuration: createMockFunctionConfig({ + FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:testFunction', + }), + }), + middlewareStack: {} as any, + destroy: () => {}, + } as any + }) + + const mockFunctionConfig: FunctionConfiguration = createMockFunctionConfig({ + FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:testFunction', + }) + + await ldkClient.getFunctionDetail(mockFunctionConfig.FunctionArn!) + + assert(createAwsServiceStubLambda.called, 'Should call createAwsService') + assert.strictEqual(capturedClientOptions.clientOptions.region, 'us-east-1', 'Should use correct region') + assert.deepStrictEqual( + capturedClientOptions.clientOptions.customUserAgent, + expectedUserAgentPairs, + 'Should include correct customUserAgent pairs with LAMBDA-DEBUG prefix in Lambda API calls' + ) + }) + + it('should create IoT client with correct user-agent', async () => { + // Restore the existing stub and create a new one to track calls + const existingStub = (utils.getIoTSTClientWithAgent as any).restore + ? (utils.getIoTSTClientWithAgent as sinon.SinonStub) + : undefined + if (existingStub) { + existingStub.restore() + } + // Stub the sdkClientBuilderV3 to capture the client options + let capturedClientOptions: any + const createAwsServiceStubIoT = userAgentSandbox.stub(globals.sdkClientBuilderV3, 'createAwsService') + createAwsServiceStubIoT.callsFake((options: any) => { + capturedClientOptions = options + return mockIoTSTClient as any + }) + + mockIoTSTClient.on(ListTunnelsCommand).resolves({ tunnelSummaries: [] }) + mockIoTSTClient.on(OpenTunnelCommand).resolves({ + tunnelId: 'tunnel-123', + sourceAccessToken: 'source-token', + destinationAccessToken: 'dest-token', + }) + + await ldkClient.createOrReuseTunnel('us-east-1') + + assert(createAwsServiceStubIoT.calledOnce, 'Should call createAwsService once') + assert.strictEqual(capturedClientOptions.clientOptions.region, 'us-east-1', 'Should use correct region') + assert.deepStrictEqual( + capturedClientOptions.clientOptions.customUserAgent, + expectedUserAgentPairs, + 'Should include correct customUserAgent pairs with LAMBDA-DEBUG prefix' + ) + }) + }) }) describe('Helper Functions', () => { diff --git a/packages/core/src/test/lambda/remoteDebugging/ldkController.test.ts b/packages/core/src/test/lambda/remoteDebugging/ldkController.test.ts index 6c2a173fdaa..3add037c1d9 100644 --- a/packages/core/src/test/lambda/remoteDebugging/ldkController.test.ts +++ b/packages/core/src/test/lambda/remoteDebugging/ldkController.test.ts @@ -6,14 +6,15 @@ import assert from 'assert' import * as vscode from 'vscode' import sinon, { SinonStubbedInstance, createStubInstance } from 'sinon' -import { Lambda } from 'aws-sdk' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' import { RemoteDebugController, - DebugConfig, activateRemoteDebugging, revertExistingConfig, - getLambdaSnapshot, + tryAutoDetectOutFile, + validateSourceMapFiles, } from '../../../lambda/remoteDebugging/ldkController' +import { getLambdaSnapshot, type DebugConfig } from '../../../lambda/remoteDebugging/lambdaDebugger' import { LdkClient } from '../../../lambda/remoteDebugging/ldkClient' import globals from '../../../shared/extensionGlobals' import * as messages from '../../../shared/utilities/messages' @@ -29,6 +30,10 @@ import { setupDebuggingState, setupMockCleanupOperations, } from './testUtils' +import { getRemoteDebugLayer } from '../../../lambda/remoteDebugging/remoteLambdaDebugger' +import { fs } from '../../../shared/fs/fs' +import * as detectCdkProjects from '../../../awsService/cdk/explorer/detectCdkProjects' +import * as glob from 'glob' describe('RemoteDebugController', () => { let sandbox: sinon.SinonSandbox @@ -98,6 +103,10 @@ describe('RemoteDebugController', () => { assert.strictEqual(controller.supportCodeDownload(undefined), false, 'Should not support undefined runtime') }) + it('should not support code download for hot-reloading LocalStack functions', () => { + assert.strictEqual(controller.supportCodeDownload('nodejs18.x', 'hot-reloading-hash-not-available'), false) + }) + it('should support remote debug for node, python, and java runtimes', () => { assert.strictEqual(controller.supportRuntimeRemoteDebug('nodejs18.x'), true, 'Should support Node.js') assert.strictEqual(controller.supportRuntimeRemoteDebug('python3.9'), true, 'Should support Python') @@ -111,7 +120,7 @@ describe('RemoteDebugController', () => { }) it('should get remote debug layer for supported regions and architectures', () => { - const result = controller.getRemoteDebugLayer('us-east-1', ['x86_64']) + const result = getRemoteDebugLayer('us-east-1', ['x86_64']) assert.strictEqual(typeof result, 'string', 'Should return layer ARN for supported region and architecture') assert(result?.includes('us-east-1'), 'Should contain the region in the ARN') @@ -119,14 +128,14 @@ describe('RemoteDebugController', () => { }) it('should return undefined for unsupported regions', () => { - const result = controller.getRemoteDebugLayer('unsupported-region', ['x86_64']) + const result = getRemoteDebugLayer('unsupported-region', ['x86_64']) assert.strictEqual(result, undefined, 'Should return undefined for unsupported region') }) it('should return undefined when region or architectures are undefined', () => { - assert.strictEqual(controller.getRemoteDebugLayer(undefined, ['x86_64']), undefined) - assert.strictEqual(controller.getRemoteDebugLayer('us-west-2', undefined), undefined) + assert.strictEqual(getRemoteDebugLayer(undefined, ['x86_64']), undefined) + assert.strictEqual(getRemoteDebugLayer('us-west-2', undefined), undefined) }) }) @@ -196,7 +205,7 @@ describe('RemoteDebugController', () => { describe('Debug Session Management', () => { let mockConfig: DebugConfig - let mockFunctionConfig: Lambda.FunctionConfiguration + let mockFunctionConfig: FunctionConfiguration beforeEach(() => { mockConfig = createMockDebugConfig({ @@ -235,7 +244,7 @@ describe('RemoteDebugController', () => { assertTelemetry('lambda_remoteDebugStart', { result: 'Succeeded', source: 'remoteDebug', - action: '{"port":9229,"remoteRoot":"/var/task","skipFiles":[],"shouldPublishVersion":false,"lambdaTimeout":900,"layerArn":"arn:aws:lambda:us-west-2:123456789012:layer:LDKLayerX86:6"}', + action: '{"port":9229,"remoteRoot":"/var/task","skipFiles":[],"shouldPublishVersion":false,"lambdaTimeout":900,"layerArn":"arn:aws:lambda:us-west-2:123456789012:layer:LDKLayerX86:6","isLambdaRemote":true}', runtimeString: 'nodejs18.x', }) }) @@ -297,7 +306,7 @@ describe('RemoteDebugController', () => { assertTelemetry('lambda_remoteDebugStart', { result: 'Succeeded', source: 'remoteDebug', - action: '{"port":9229,"remoteRoot":"/var/task","skipFiles":[],"shouldPublishVersion":true,"lambdaTimeout":900,"layerArn":"arn:aws:lambda:us-west-2:123456789012:layer:LDKLayerX86:6"}', + action: '{"port":9229,"remoteRoot":"/var/task","skipFiles":[],"shouldPublishVersion":true,"lambdaTimeout":900,"layerArn":"arn:aws:lambda:us-west-2:123456789012:layer:LDKLayerX86:6","isLambdaRemote":true}', runtimeString: 'nodejs18.x', }) }) @@ -402,7 +411,7 @@ describe('RemoteDebugController', () => { describe('Telemetry Verification', () => { let mockConfig: DebugConfig - let mockFunctionConfig: Lambda.FunctionConfiguration + let mockFunctionConfig: FunctionConfiguration beforeEach(() => { mockConfig = createMockDebugConfig({ @@ -436,13 +445,299 @@ describe('RemoteDebugController', () => { assertTelemetry('lambda_remoteDebugStart', { result: 'Failed', source: 'remoteDebug', - action: '{"port":9229,"remoteRoot":"/var/task","skipFiles":[],"shouldPublishVersion":false,"lambdaTimeout":900,"layerArn":"arn:aws:lambda:us-west-2:123456789012:layer:LDKLayerX86:6"}', + action: '{"port":9229,"remoteRoot":"/var/task","skipFiles":[],"shouldPublishVersion":false,"lambdaTimeout":900,"layerArn":"arn:aws:lambda:us-west-2:123456789012:layer:LDKLayerX86:6","isLambdaRemote":true}', runtimeString: 'nodejs18.x', }) }) }) }) +describe('tryAutoDetectOutFile', () => { + let sandbox: sinon.SinonSandbox + + // Common test constants + const testFunctionName = 'TestFunction' + const testSamProjectRoot = vscode.Uri.file('/path/to/sam-project') + const testSamLogicalId = 'MyFunction' + const testCdkProjectRoot = vscode.Uri.file('/path/to/cdk-project') + const testCdkAssetPath = 'asset.728566f9cc2388f3c89a024fd2e887b4d82715454a0fc478f57d7d034364fdd5' + const testCdkOutDir = vscode.Uri.joinPath(testCdkProjectRoot, 'cdk.out') + const testMockWorkspaceFolder: vscode.WorkspaceFolder = { + uri: testCdkProjectRoot, + name: 'cdk-project', + index: 0, + } + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + it('should return undefined for non-TypeScript files', async () => { + const debugConfig: DebugConfig = createMockDebugConfig({ + handlerFile: '/path/to/handler.js', // JavaScript file, not TypeScript + }) + const functionConfig: FunctionConfiguration = createMockFunctionConfig() + + const result = await tryAutoDetectOutFile(debugConfig, functionConfig) + + assert.strictEqual(result, undefined, 'Should return undefined for non-TypeScript files') + }) + + it('should return undefined when handlerFile is not provided', async () => { + const debugConfig: DebugConfig = createMockDebugConfig({ + handlerFile: undefined, + }) + const functionConfig: FunctionConfiguration = createMockFunctionConfig() + + const result = await tryAutoDetectOutFile(debugConfig, functionConfig) + + assert.strictEqual(result, undefined, 'Should return undefined when handlerFile is not provided') + }) + + it('should detect SAM build path when SAM parameters are provided', async () => { + const expectedPath = vscode.Uri.joinPath(testSamProjectRoot, '.aws-sam', 'build', testSamLogicalId) + + const debugConfig: DebugConfig = createMockDebugConfig({ + handlerFile: '/path/to/handler.ts', + samProjectRoot: testSamProjectRoot, + samFunctionLogicalId: testSamLogicalId, + }) + const functionConfig: FunctionConfiguration = createMockFunctionConfig() + + // Mock fs.exists to return true for SAM build path + sandbox.stub(fs, 'exists').resolves(true) + + const result = await tryAutoDetectOutFile(debugConfig, functionConfig) + + assert.strictEqual(result, expectedPath.fsPath, 'Should return SAM build path') + }) + + it('should return undefined when SAM build path does not exist', async () => { + const debugConfig: DebugConfig = createMockDebugConfig({ + handlerFile: '/path/to/handler.ts', + samProjectRoot: testSamProjectRoot, + samFunctionLogicalId: testSamLogicalId, + }) + const functionConfig: FunctionConfiguration = createMockFunctionConfig() + + // Mock fs.exists to return false + sandbox.stub(fs, 'exists').resolves(false) + + const result = await tryAutoDetectOutFile(debugConfig, functionConfig) + + assert.strictEqual(result, undefined, 'Should return undefined when SAM build path does not exist') + }) + + it('should detect CDK asset path from template.json', async () => { + const expectedAssetDir = vscode.Uri.joinPath(testCdkOutDir, testCdkAssetPath) + + const debugConfig: DebugConfig = createMockDebugConfig({ + handlerFile: '/path/to/cdk-project/src/handler.ts', + }) + const functionConfig: FunctionConfiguration = createMockFunctionConfig({ + FunctionName: testFunctionName, + }) + + // Mock workspace folder + sandbox.stub(vscode.workspace, 'getWorkspaceFolder').returns(testMockWorkspaceFolder) + + // Mock CDK project detection + const detectCdkProjectsStub = sandbox.stub(detectCdkProjects, 'detectCdkProjects') + detectCdkProjectsStub.resolves([ + { + cdkJsonUri: vscode.Uri.joinPath(testMockWorkspaceFolder.uri, 'cdk.json'), + treeUri: vscode.Uri.joinPath(testCdkOutDir, 'tree.json'), + }, + ]) + + // Mock finding template files + sandbox + .stub(vscode.workspace, 'findFiles') + .resolves([vscode.Uri.joinPath(testCdkOutDir, 'stack.template.json')]) + + // Mock reading template file + const mockTemplate = { + Resources: { + MyFunctionB75F74F2: { + Type: 'AWS::Lambda::Function', + Properties: { + FunctionName: testFunctionName, + }, + Metadata: { + 'aws:asset:path': testCdkAssetPath, + }, + }, + }, + } + const readTextStub = sandbox.stub(fs, 'readFileText') + readTextStub.resolves(JSON.stringify(mockTemplate)) + sandbox.stub(fs, 'exists').resolves(true) + + const result = await tryAutoDetectOutFile(debugConfig, functionConfig) + + assert.strictEqual(result, expectedAssetDir.fsPath, 'Should return CDK asset directory path') + + const functionNonExistConfig: FunctionConfiguration = createMockFunctionConfig({ + FunctionName: 'NonExistentFunction', + }) + const result2 = await tryAutoDetectOutFile(debugConfig, functionNonExistConfig) + + assert.strictEqual(result2, undefined, 'Should return undefined when function not found in template') + + readTextStub.resolves('{ invalid json }') + + const result3 = await tryAutoDetectOutFile(debugConfig, functionConfig) + + assert.strictEqual(result3, undefined, 'Should return undefined on template parsing error') + }) + + it('should return undefined when no workspace folder is found', async () => { + const debugConfig: DebugConfig = createMockDebugConfig({ + handlerFile: '/path/to/handler.ts', + }) + const functionConfig: FunctionConfiguration = createMockFunctionConfig() + + // Mock no workspace folder + sandbox.stub(vscode.workspace, 'getWorkspaceFolder').returns(undefined) + + const result = await tryAutoDetectOutFile(debugConfig, functionConfig) + + assert.strictEqual(result, undefined, 'Should return undefined when no workspace folder') + }) + + it('should prioritize SAM detection over CDK detection', async () => { + const samPath = vscode.Uri.joinPath(testSamProjectRoot, '.aws-sam', 'build', testSamLogicalId) + + const debugConfig: DebugConfig = createMockDebugConfig({ + handlerFile: '/path/to/handler.ts', + samProjectRoot: testSamProjectRoot, + samFunctionLogicalId: testSamLogicalId, + }) + const functionConfig: FunctionConfiguration = createMockFunctionConfig({ + FunctionName: testFunctionName, + }) + + // Mock fs.exists to return true for SAM path + const existsStub = sandbox.stub(fs, 'exists') + existsStub.withArgs(samPath).resolves(true) + + // Even though we could detect CDK, SAM should be prioritized + const result = await tryAutoDetectOutFile(debugConfig, functionConfig) + + assert.strictEqual(result, samPath.fsPath, 'Should prioritize SAM detection over CDK') + }) + + it('should handle .tsx TypeScript files', async () => { + const expectedPath = vscode.Uri.joinPath(testSamProjectRoot, '.aws-sam', 'build', testSamLogicalId) + + const debugConfig: DebugConfig = createMockDebugConfig({ + handlerFile: '/path/to/handler.tsx', // TSX file + samProjectRoot: testSamProjectRoot, + samFunctionLogicalId: testSamLogicalId, + }) + const functionConfig: FunctionConfiguration = createMockFunctionConfig() + + // Mock fs.exists to return true + sandbox.stub(fs, 'exists').resolves(true) + + const result = await tryAutoDetectOutFile(debugConfig, functionConfig) + + assert.strictEqual(result, expectedPath.fsPath, 'Should handle .tsx files') + }) +}) + +describe('Source Map Pattern Extraction', () => { + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + it('should extract temp patterns from source map files', async () => { + assert(vscode.workspace.workspaceFolders?.[0]?.uri, 'Test env should have a workdir') + const testPath = vscode.Uri.joinPath(vscode.workspace.workspaceFolders?.[0]?.uri, 'remote-debug-ts-app').fsPath + + // Call validateSourceMapFiles which will extract temp patterns + const result = await validateSourceMapFiles([`${testPath}/*`]) + + assert(result.isValid, 'Should find valid source map files') + assert(result.tempPatterns.has('tmp5bmwuffn'), 'Should extract temp pattern tmp5bmwuffn from source map') + }) + + it('should handle multiple temp patterns in source maps', async () => { + // Create a mock source map with multiple temp patterns + // Updated to use lowercase and underscore patterns matching Python's tempfile.mkdtemp() + const mockSourceMap = { + version: 3, + file: 'index.js', + sources: [ + '../../../../../../tmpa1b2c3d4/index.ts', + '../../../../../../tmpx9y8_7w6/utils.ts', + '../../../../../../tmp_test123/helper.ts', + '/var/task/regular-path.ts', // This should not match + ], + mappings: 'AAAA', + } + + // Mock fs.readFileText to return our mock source map + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(mockSourceMap)) + + // Call extractTempPatternsFromSourceMaps directly (we need to export it first) + // For now, we'll test through validateSourceMapFiles + + sandbox.stub(glob, 'glob').resolves(['/test/path/index.js', '/test/path/index.js.map']) + + const result = await validateSourceMapFiles(['/test/path/*']) + + assert(result.isValid, 'Should be valid') + assert(result.tempPatterns.has('tmpa1b2c3d4'), 'Should extract first temp pattern') + assert(result.tempPatterns.has('tmpx9y8_7w6'), 'Should extract second temp pattern') + assert(result.tempPatterns.has('tmp_test123'), 'Should extract third temp pattern') + assert.strictEqual(result.tempPatterns.size, 3, 'Should have exactly 3 temp patterns') + }) + + it('should handle source maps without temp patterns', async () => { + // Create a mock source map without temp patterns + const mockSourceMap = { + version: 3, + file: 'index.js', + sources: ['./index.ts', '../utils/helper.ts'], + mappings: 'AAAA', + } + + // Mock fs.readFileText + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(mockSourceMap)) + + // Mock glob + sandbox.stub(glob, 'glob').resolves(['/test/path/index.js', '/test/path/index.js.map']) + + const result = await validateSourceMapFiles(['/test/path/*']) + + assert(result.isValid, 'Should be valid even without temp patterns') + assert.strictEqual(result.tempPatterns.size, 0, 'Should have no temp patterns') + }) + + it('should handle malformed source map files gracefully', async () => { + // Mock fs.readFileText to return invalid JSON + sandbox.stub(fs, 'readFileText').resolves('{ invalid json }') + + sandbox.stub(glob, 'glob').resolves(['/test/path/index.js', '/test/path/index.js.map']) + + const result = await validateSourceMapFiles(['/test/path/*']) + + assert(result.isValid, 'Should still be valid despite malformed source map') + assert.strictEqual(result.tempPatterns.size, 0, 'Should have no temp patterns when parsing fails') + }) +}) + describe('Module Functions', () => { let sandbox: sinon.SinonSandbox let mockGlobalState: any diff --git a/packages/core/src/test/lambda/remoteDebugging/localStackLambdaDebugger.test.ts b/packages/core/src/test/lambda/remoteDebugging/localStackLambdaDebugger.test.ts new file mode 100644 index 00000000000..d91fc150f71 --- /dev/null +++ b/packages/core/src/test/lambda/remoteDebugging/localStackLambdaDebugger.test.ts @@ -0,0 +1,240 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import assert from 'assert' +import sinon, { SinonStubbedInstance, createStubInstance } from 'sinon' +import { LdkClient } from '../../../lambda/remoteDebugging/ldkClient' +import { RemoteDebugController } from '../../../lambda/remoteDebugging/ldkController' +import globals from '../../../shared/extensionGlobals' + +import { + createMockDebugConfig, + createMockFunctionConfig, + createMockGlobalState, + setupMockRevertExistingConfig, + setupMockVSCodeDebugAPIs, +} from './testUtils' +import { DebugConfig } from '../../../lambda/remoteDebugging/lambdaDebugger' +import { FunctionConfiguration, Runtime } from '@aws-sdk/client-lambda' +import { assertTelemetry } from '../../testUtil' +import * as remoteDebuggingUtils from '../../../lambda/remoteDebugging/utils' +import { DefaultLambdaClient } from '../../../shared/clients/lambdaClient' + +const LocalStackEndpoint = 'https://localhost.localstack.cloud:4566' + +describe('RemoteDebugController with LocalStackLambdaDebugger', () => { + let sandbox: sinon.SinonSandbox + let mockLdkClient: SinonStubbedInstance + let controller: RemoteDebugController + let mockGlobalState: any + let mockConfig: DebugConfig + let mockFunctionConfig: FunctionConfiguration + let fetchStub: sinon.SinonStub + + beforeEach(() => { + sandbox = sinon.createSandbox() + + fetchStub = sandbox.stub(global, 'fetch') + + // Mock LdkClient + mockLdkClient = createStubInstance(LdkClient) + sandbox.stub(LdkClient, 'instance').get(() => mockLdkClient) + + // Mock global state with actual storage + mockGlobalState = createMockGlobalState() + sandbox.stub(globals, 'globalState').value(mockGlobalState) + sandbox.stub(globals.awsContext, 'getCredentialEndpointUrl').returns(LocalStackEndpoint) + + // Get controller instance + controller = RemoteDebugController.instance + + // Ensure clean state + controller.ensureCleanState() + + mockConfig = createMockDebugConfig({ + isLambdaRemote: false, + port: undefined, + layerArn: undefined, + lambdaTimeout: undefined, + }) + mockFunctionConfig = createMockFunctionConfig({ Runtime: 'nodejs22.x' as Runtime }) + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('Debug Session Management', () => { + it('should start debugging successfully', async () => { + // Mock VSCode APIs + setupMockVSCodeDebugAPIs(sandbox) + + // Mock runtime support + sandbox.stub(controller, 'supportRuntimeRemoteDebug').returns(true) + + // Mock successful LdkClient operations + mockLdkClient.getFunctionDetail.resolves(mockFunctionConfig) + + // Mock waiting for Lambda function to be active + sandbox.stub(remoteDebuggingUtils, 'getLambdaClientWithAgent').returns( + sandbox.createStubInstance(DefaultLambdaClient, { + waitForActive: sandbox.stub().resolves() as any, + }) as any + ) + + // Mock revertExistingConfig + setupMockRevertExistingConfig(sandbox) + + // Mock LocalStack health check + const fetchStubHealth = fetchStub.withArgs(`${LocalStackEndpoint}/_localstack/health`) + fetchStubHealth.resolves(new Response(undefined, { status: 200 })) + + // Mock LocalStack debug config setup + const assignedPort = 8228 + const userAgent = + 'LAMBDA-DEBUG/1.0.0 AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.102.2 ClientId/11111111-1111-1111-1111-111111111111' + const fetchStubSetup = fetchStub.withArgs( + `${LocalStackEndpoint}/_aws/lambda/debug_configs/${mockFunctionConfig.FunctionArn}:$LATEST`, + { + method: 'PUT', + body: sinon.match.string, + } + ) + fetchStubSetup.resolves( + new Response( + JSON.stringify({ + port: assignedPort, + user_agent: userAgent, + }), + { status: 200 } + ) + ) + + // Mock LocalStack debug config polling + const fetchStubStatus = fetchStub.withArgs( + `${LocalStackEndpoint}/_aws/lambda/debug_configs/${mockFunctionConfig.FunctionArn}:$LATEST?debug_server_ready_timeout=300` + ) + fetchStubStatus.resolves( + new Response( + JSON.stringify({ + port: assignedPort, + user_agent: userAgent, + is_debug_server_running: true, + }), + { status: 200 } + ) + ) + + await controller.startDebugging(mockConfig.functionArn, 'nodejs22.x', mockConfig) + + // Assert state changes + assert.strictEqual(controller.isDebugging, true, 'Should be in debugging state') + // Qualifier is not set for LocalStack + assert.strictEqual(controller.qualifier, undefined, 'Should not set qualifier for $LATEST') + + assert(mockLdkClient.getFunctionDetail.calledWith(mockConfig.functionArn), 'Should get function details') + + assert(fetchStubHealth.calledOnce, 'Should call LocalStack health check once') + assert(fetchStubSetup.calledOnce, 'Should call LocalStack LDM setup once') + assert(fetchStubStatus.calledOnce, 'Should call LocalStack LDM status once') + + assertTelemetry('lambda_remoteDebugStart', { + result: 'Succeeded', + source: 'LocalStackDebug', + action: '{"remoteRoot":"/var/task","skipFiles":[],"shouldPublishVersion":false,"isLambdaRemote":false}', + runtimeString: 'nodejs22.x', + }) + }) + + it('should handle debugging start failure and cleanup', async () => { + // Mock VSCode APIs + setupMockVSCodeDebugAPIs(sandbox) + + // Mock runtime support + sandbox.stub(controller, 'supportRuntimeRemoteDebug').returns(true) + + // Mock function config retrieval + mockLdkClient.getFunctionDetail.resolves(mockFunctionConfig) + + // Mock LocalStack health check + const fetchStubHealth = fetchStub.withArgs(`${LocalStackEndpoint}/_localstack/health`) + fetchStubHealth.resolves(new Response(undefined, { status: 200 })) + + // Mock LocalStack debug config setup error + const fetchStubSetup = fetchStub.withArgs( + `${LocalStackEndpoint}/_aws/lambda/debug_configs/${mockFunctionConfig.FunctionArn}:$LATEST`, + { + method: 'PUT', + body: sinon.match.string, + } + ) + fetchStubSetup.resolves(new Response('Unknown error occurred during setup', { status: 500 })) + + // Mock LocalStack debug config cleanup + const fetchStubCleanup = fetchStub.withArgs( + `${LocalStackEndpoint}/_aws/lambda/debug_configs/${mockFunctionConfig.FunctionArn}:$LATEST`, + { + method: 'DELETE', + } + ) + fetchStubCleanup.resolves(new Response(undefined, { status: 200 })) + + // Mock revertExistingConfig + setupMockRevertExistingConfig(sandbox) + + try { + await controller.startDebugging(mockConfig.functionArn, 'nodejs22.x', mockConfig) + assert.fail('Should have thrown an error') + } catch (error) { + assert(error instanceof Error, 'Should throw an error') + assert( + error.message.includes('Error StartDebugging') || + error.message.includes( + 'Failed to startup execution environment or debugger for Lambda function' + ), + 'Should throw relevant error' + ) + } + + // Assert state is cleaned up + assert.strictEqual(controller.isDebugging, false, 'Should not be in debugging state after failure') + assert(fetchStubCleanup.calledOnce, 'Should attempt cleanup') + }) + }) + + describe('Stop Debugging', () => { + it('should stop debugging successfully', async () => { + // Mock VSCode APIs + sandbox.stub(vscode.commands, 'executeCommand').resolves() + + // Set up debugging state + controller.isDebugging = true + controller.qualifier = '$LATEST' + ;(controller as any).lastDebugStartTime = Date.now() - 5000 // 5 seconds ago + mockGlobalState.update('aws.lambda.remoteDebugSnapshot', mockFunctionConfig) + + // Mock successful cleanup + const fetchStubCleanup = fetchStub.withArgs( + `${LocalStackEndpoint}/_aws/lambda/debug_configs/${mockFunctionConfig.FunctionArn}:$LATEST`, + { + method: 'DELETE', + } + ) + fetchStubCleanup.resolves(new Response(undefined, { status: 200 })) + + await controller.stopDebugging() + + // Assert state is cleaned up + assert.strictEqual(controller.isDebugging, false, 'Should not be in debugging state') + + // Verify cleanup operations + assert(fetchStubCleanup.calledOnce, 'Should cleanup the LocalStack debug config') + assertTelemetry('lambda_remoteDebugStop', { + result: 'Succeeded', + }) + }) + }) +}) diff --git a/packages/core/src/test/lambda/remoteDebugging/testUtils.ts b/packages/core/src/test/lambda/remoteDebugging/testUtils.ts index 67a53b15d61..42deaa4ec96 100644 --- a/packages/core/src/test/lambda/remoteDebugging/testUtils.ts +++ b/packages/core/src/test/lambda/remoteDebugging/testUtils.ts @@ -4,27 +4,25 @@ */ import sinon from 'sinon' -import { Lambda } from 'aws-sdk' +import { Architecture, FunctionConfiguration, Runtime, SnapStartApplyOn } from '@aws-sdk/client-lambda' import { LambdaFunctionNode } from '../../../lambda/explorer/lambdaFunctionNode' import { InitialData } from '../../../lambda/vue/remoteInvoke/invokeLambda' -import { DebugConfig } from '../../../lambda/remoteDebugging/ldkController' +import type { DebugConfig } from '../../../lambda/remoteDebugging/lambdaDebugger' /** * Creates a mock Lambda function configuration for testing */ -export function createMockFunctionConfig( - overrides: Partial = {} -): Lambda.FunctionConfiguration { +export function createMockFunctionConfig(overrides: Partial = {}): FunctionConfiguration { return { FunctionName: 'testFunction', FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', - Runtime: 'nodejs18.x', + Runtime: Runtime.nodejs18x, Handler: 'index.handler', Timeout: 30, Layers: [], Environment: { Variables: {} }, - Architectures: ['x86_64'], - SnapStart: { ApplyOn: 'None' }, + Architectures: [Architecture.x86_64], + SnapStart: { ApplyOn: SnapStartApplyOn.None }, ...overrides, } } @@ -76,6 +74,7 @@ export function createMockDebugConfig(overrides: Partial = {}): Deb shouldPublishVersion: false, lambdaTimeout: 900, layerArn: 'arn:aws:lambda:us-west-2:123456789012:layer:LDKLayerX86:6', + isLambdaRemote: true, ...overrides, } } diff --git a/packages/core/src/test/lambda/utils.test.ts b/packages/core/src/test/lambda/utils.test.ts index a3eebe043a7..7a8e82043cf 100644 --- a/packages/core/src/test/lambda/utils.test.ts +++ b/packages/core/src/test/lambda/utils.test.ts @@ -18,6 +18,7 @@ import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' import { fs } from '../../shared/fs/fs' import { tempDirPath } from '../../shared/filesystemUtilities' import path from 'path' +import { Runtime } from '@aws-sdk/client-lambda' describe('lambda utils', function () { const mockLambda = { @@ -67,7 +68,7 @@ describe('lambda utils', function () { }) ) // runtime that isn't present, period - assert.throws(() => getLambdaDetails({ Runtime: 'COBOL-60', Handler: 'asdf.asdf' })) + assert.throws(() => getLambdaDetails({ Runtime: 'COBOL-60' as Runtime, Handler: 'asdf.asdf' })) }) }) diff --git a/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambda.test.ts b/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambda.test.ts index 1b9f4bfde8e..c39238304f3 100644 --- a/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambda.test.ts +++ b/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambda.test.ts @@ -22,12 +22,14 @@ import * as samCliRemoteTestEvent from '../../../../shared/sam/cli/samCliRemoteT import { TestEventsOperation, SamCliRemoteTestEventsParameters } from '../../../../shared/sam/cli/samCliRemoteTestEvent' import { assertLogsContain } from '../../../globalSetup.test' import { createResponse } from '../../../testUtil' +import { InvocationResponse } from '@aws-sdk/client-lambda' describe('RemoteInvokeWebview', () => { let outputChannel: vscode.OutputChannel let client: SinonStubbedInstance let remoteInvokeWebview: RemoteInvokeWebview let data: InitialData + let sandbox: sinon.SinonSandbox beforeEach(() => { client = createStubInstance(DefaultLambdaClient) @@ -40,9 +42,14 @@ describe('RemoteInvokeWebview', () => { FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', FunctionRegion: 'us-west-2', InputSamples: [], + LambdaFunctionNode: { + configuration: { + State: 'Active', + }, + } as LambdaFunctionNode, } as InitialData - remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, data) + remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, client, data) }) describe('init', () => { it('should return the data property', () => { @@ -51,6 +58,11 @@ describe('RemoteInvokeWebview', () => { FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', FunctionRegion: 'us-west-2', InputSamples: [], + LambdaFunctionNode: { + configuration: { + State: 'Active', + }, + } as LambdaFunctionNode, } const result = remoteInvokeWebview.init() assert.deepEqual(result, mockData) @@ -61,8 +73,8 @@ describe('RemoteInvokeWebview', () => { const input = '{"key": "value"}' const mockResponse = { LogResult: Buffer.from('Test log').toString('base64'), - Payload: '{"result": "success"}', - } + Payload: new TextEncoder().encode('{"result": "success"}'), + } satisfies InvocationResponse client.invoke.resolves(mockResponse) const appendedLines: string[] = [] @@ -72,7 +84,7 @@ describe('RemoteInvokeWebview', () => { await remoteInvokeWebview.invokeLambda(input) assert(client.invoke.calledOnce) - assert(client.invoke.calledWith(data.FunctionArn, input)) + assert(client.invoke.calledWith(data.FunctionArn, input, sinon.match.any, 'Tail')) assert.deepStrictEqual(appendedLines, [ 'Loading response...', 'Invocation result for arn:aws:lambda:us-west-2:123456789012:function:testFunction', @@ -87,8 +99,8 @@ describe('RemoteInvokeWebview', () => { it('handles Lambda invocation with no payload', async () => { const mockResponse = { LogResult: Buffer.from('Test log').toString('base64'), - Payload: '', - } + Payload: new TextEncoder().encode(''), + } satisfies InvocationResponse client.invoke.resolves(mockResponse) const appendedLines: string[] = [] @@ -111,8 +123,8 @@ describe('RemoteInvokeWebview', () => { }) it('handles Lambda invocation with undefined LogResult', async () => { const mockResponse = { - Payload: '{"result": "success"}', - } + Payload: new TextEncoder().encode('{"result": "success"}'), + } satisfies InvocationResponse client.invoke.resolves(mockResponse) @@ -150,10 +162,7 @@ describe('RemoteInvokeWebview', () => { assert.fail('Expected an error to be thrown') } catch (err) { assert.ok(err instanceof Error) - assert.strictEqual( - err.message, - 'telemetry: invalid Metric: "lambda_invokeRemote" emitted with result=Failed but without the `reason` property. Consider using `.run()` instead of `.emit()`, which will set these properties automatically. See https://github.com/aws/aws-toolkit-vscode/blob/master/docs/telemetry.md#guidelines' - ) + assert.strictEqual(err.message, 'Expected an error to be thrown') } assert.deepStrictEqual(appendedLines, [ @@ -243,6 +252,377 @@ describe('RemoteInvokeWebview', () => { }) }) + describe('Remote Test Events', () => { + let runSamCliStub: sinon.SinonStub + sandbox = sinon.createSandbox() + beforeEach(() => { + runSamCliStub = sandbox.stub(samCliRemoteTestEvent, 'runSamCliRemoteTestEvents') + // Mock getSamCliContext module + const samCliContext = require('../../../../shared/sam/cli/samCliContext') + sandbox.stub(samCliContext, 'getSamCliContext').returns({ + invoker: {} as any, + }) + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('listRemoteTestEvents', () => { + it('should list remote test events successfully', async () => { + runSamCliStub.resolves('event1\nevent2\nevent3\n') + + const events = await remoteInvokeWebview.listRemoteTestEvents(data.FunctionArn, data.FunctionRegion) + + assert.deepStrictEqual(events, ['event1', 'event2', 'event3']) + assert(runSamCliStub.calledOnce) + assert( + runSamCliStub.calledWith( + sinon.match({ + functionArn: data.FunctionArn, + operation: 'list', + region: data.FunctionRegion, + }) + ) + ) + }) + + it('should return empty array when no events exist (registry not found)', async () => { + runSamCliStub.rejects(new Error('lambda-testevent-schemas registry not found')) + + const events = await remoteInvokeWebview.listRemoteTestEvents(data.FunctionArn, data.FunctionRegion) + + assert.deepStrictEqual(events, []) + }) + + it('should return empty array when there are no saved events', async () => { + runSamCliStub.rejects(new Error('There are no saved events')) + + const events = await remoteInvokeWebview.listRemoteTestEvents(data.FunctionArn, data.FunctionRegion) + + assert.deepStrictEqual(events, []) + }) + + it('should re-throw other errors', async () => { + runSamCliStub.rejects(new Error('Network error')) + + await assert.rejects( + async () => await remoteInvokeWebview.listRemoteTestEvents(data.FunctionArn, data.FunctionRegion), + /Network error/ + ) + }) + }) + + describe('selectRemoteTestEvent', () => { + it('should show quickpick and return selected event content', async () => { + // Mock list events + runSamCliStub.onFirstCall().resolves('event1\nevent2\n') + // Mock get event content + runSamCliStub.onSecondCall().resolves('{"test": "content"}') + + // Mock quickpick selection using test window + getTestWindow().onDidShowQuickPick((picker) => { + picker.acceptItem('event1') + }) + + const result = await remoteInvokeWebview.selectRemoteTestEvent(data.FunctionArn, data.FunctionRegion) + + assert.strictEqual(result, '{"test": "content"}') + }) + + it('should show info message when no events exist', async () => { + runSamCliStub.onFirstCall().resolves('') + + let infoMessageShown = false + getTestWindow().onDidShowMessage((message) => { + if (message.message.includes('No remote test events found')) { + infoMessageShown = true + } + }) + + const result = await remoteInvokeWebview.selectRemoteTestEvent(data.FunctionArn, data.FunctionRegion) + + assert.strictEqual(result, undefined) + assert(infoMessageShown, 'Info message should be shown') + }) + + it('should return undefined when user cancels quickpick', async () => { + runSamCliStub.onFirstCall().resolves('event1\nevent2\n') + + // Mock user canceling quickpick + getTestWindow().onDidShowQuickPick((picker) => { + picker.hide() + }) + + const result = await remoteInvokeWebview.selectRemoteTestEvent(data.FunctionArn, data.FunctionRegion) + + assert.strictEqual(result, undefined) + }) + + it('should handle list events error gracefully', async () => { + runSamCliStub.rejects(new Error('API error')) + + let errorMessageShown = false + getTestWindow().onDidShowMessage((message) => { + // Check if it's an error message + errorMessageShown = true + }) + + const result = await remoteInvokeWebview.selectRemoteTestEvent(data.FunctionArn, data.FunctionRegion) + + assert.strictEqual(result, undefined) + assert(errorMessageShown, 'Error message should be shown') + }) + }) + + describe('saveRemoteTestEvent', () => { + it('should create new test event', async () => { + // Mock empty list (no existing events) + runSamCliStub.onFirstCall().resolves('') + // Mock create event success + runSamCliStub.onSecondCall().resolves('Event created') + + // Mock quickpick to select "Create new" + getTestWindow().onDidShowQuickPick((picker) => { + picker.acceptItem('$(add) Create new test event') + }) + + // Mock input box for event name + getTestWindow().onDidShowInputBox((input) => { + input.acceptValue('MyNewEvent') + }) + + const result = await remoteInvokeWebview.saveRemoteTestEvent( + data.FunctionArn, + data.FunctionRegion, + '{"test": "data"}' + ) + + assert.strictEqual(result, 'MyNewEvent') + assert(runSamCliStub.calledTwice) + assert( + runSamCliStub.secondCall.calledWith( + sinon.match({ + functionArn: data.FunctionArn, + operation: 'put', + name: 'MyNewEvent', + eventSample: '{"test": "data"}', + region: data.FunctionRegion, + force: false, + }) + ) + ) + }) + + it('should overwrite existing test event with force flag', async () => { + // Mock list with existing events + runSamCliStub.onFirstCall().resolves('existingEvent1\nexistingEvent2\n') + // Mock update event success + runSamCliStub.onSecondCall().resolves('Event updated') + + // Mock quickpick to select existing event + getTestWindow().onDidShowQuickPick((picker) => { + picker.acceptItem('existingEvent1') + }) + + // Mock confirmation dialog + getTestWindow().onDidShowMessage((message) => { + // Select the overwrite option + message.selectItem('Overwrite') + }) + + const result = await remoteInvokeWebview.saveRemoteTestEvent( + data.FunctionArn, + data.FunctionRegion, + '{"updated": "data"}' + ) + + assert.strictEqual(result, 'existingEvent1') + assert(runSamCliStub.calledTwice) + assert( + runSamCliStub.secondCall.calledWith( + sinon.match({ + functionArn: data.FunctionArn, + operation: 'put', + name: 'existingEvent1', + eventSample: '{"updated": "data"}', + region: data.FunctionRegion, + force: true, // Should use force flag for overwrite + }) + ) + ) + }) + + it('should handle user cancellation of overwrite', async () => { + runSamCliStub.onFirstCall().resolves('existingEvent1\n') + + // Mock quickpick to select existing event + getTestWindow().onDidShowQuickPick((picker) => { + picker.acceptItem('existingEvent1') + }) + + // User cancels overwrite warning + getTestWindow().onDidShowMessage((message) => { + // Cancel the dialog + message.close() + }) + + const result = await remoteInvokeWebview.saveRemoteTestEvent( + data.FunctionArn, + data.FunctionRegion, + '{"test": "data"}' + ) + + assert.strictEqual(result, undefined) + assert(runSamCliStub.calledOnce) // Only list was called + }) + + it('should validate event name for new events', async () => { + runSamCliStub.onFirstCall().resolves('existingEvent\n') + runSamCliStub.onSecondCall().resolves('Event created') + + // Mock quickpick to select "Create new" + getTestWindow().onDidShowQuickPick((picker) => { + picker.acceptItem('$(add) Create new test event') + }) + + // Mock input box with validation + let validationTested = false + getTestWindow().onDidShowInputBox((input) => { + // We can't directly test validation in this test framework + // Just accept a valid value + input.acceptValue('NewEvent') + validationTested = true + }) + + const result = await remoteInvokeWebview.saveRemoteTestEvent( + data.FunctionArn, + data.FunctionRegion, + '{"test": "data"}' + ) + + assert.strictEqual(result, 'NewEvent') + assert(validationTested, 'Input box should have been shown') + }) + + it('should handle list events error gracefully', async () => { + // List events fails but should continue + runSamCliStub.onFirstCall().rejects(new Error('List failed')) + runSamCliStub.onSecondCall().resolves('Event created') + + // Mock quickpick to select "Create new" + getTestWindow().onDidShowQuickPick((picker) => { + picker.acceptItem('$(add) Create new test event') + }) + + // Mock input box for event name + getTestWindow().onDidShowInputBox((input) => { + input.acceptValue('NewEvent') + }) + + const result = await remoteInvokeWebview.saveRemoteTestEvent( + data.FunctionArn, + data.FunctionRegion, + '{"test": "data"}' + ) + + assert.strictEqual(result, 'NewEvent') + // Should still create the event even if list failed + assert(runSamCliStub.calledTwice) + }) + + it('should return undefined when user cancels quickpick', async () => { + runSamCliStub.onFirstCall().resolves('event1\n') + + // Mock user canceling quickpick + getTestWindow().onDidShowQuickPick((picker) => { + picker.hide() + }) + + const result = await remoteInvokeWebview.saveRemoteTestEvent( + data.FunctionArn, + data.FunctionRegion, + '{"test": "data"}' + ) + + assert.strictEqual(result, undefined) + }) + }) + + describe('createRemoteTestEvents', () => { + it('should create event without force flag', async () => { + runSamCliStub.resolves('Event created') + + const result = await remoteInvokeWebview.createRemoteTestEvents({ + name: 'TestEvent', + event: '{"test": "data"}', + region: 'us-west-2', + arn: data.FunctionArn, + }) + + assert.strictEqual(result, 'Event created') + assert( + runSamCliStub.calledWith( + sinon.match({ + functionArn: data.FunctionArn, + operation: 'put', + name: 'TestEvent', + eventSample: '{"test": "data"}', + region: 'us-west-2', + force: false, + }) + ) + ) + }) + + it('should create event with force flag for overwrite', async () => { + runSamCliStub.resolves('Event updated') + + const result = await remoteInvokeWebview.createRemoteTestEvents( + { + name: 'ExistingEvent', + event: '{"updated": "data"}', + region: 'us-west-2', + arn: data.FunctionArn, + }, + true // force flag + ) + + assert.strictEqual(result, 'Event updated') + assert( + runSamCliStub.calledWith( + sinon.match({ + force: true, + }) + ) + ) + }) + }) + + describe('getRemoteTestEvents', () => { + it('should get remote test event content', async () => { + runSamCliStub.resolves('{"event": "content"}') + + const result = await remoteInvokeWebview.getRemoteTestEvents({ + name: 'TestEvent', + region: 'us-west-2', + arn: data.FunctionArn, + }) + + assert.strictEqual(result, '{"event": "content"}') + assert( + runSamCliStub.calledWith( + sinon.match({ + name: 'TestEvent', + operation: 'get', + functionArn: data.FunctionArn, + region: 'us-west-2', + }) + ) + ) + }) + }) + }) describe('listRemoteTestEvents', () => { let runSamCliRemoteTestEventsStub: sinon.SinonStub beforeEach(() => { @@ -303,6 +683,7 @@ describe('RemoteInvokeWebview', () => { name: mockPutEvent.name, eventSample: mockPutEvent.event, region: mockPutEvent.region, + force: false, // Default value when not overwriting } assert(runSamCliRemoteTestEventsStub.calledOnce, 'remoteTestEvents should be called once') assert( @@ -323,6 +704,29 @@ describe('RemoteInvokeWebview', () => { const result = await remoteInvokeWebview.createRemoteTestEvents(mockPutEvent) assert.strictEqual(result, mockResponse, 'The result should match the mock response') }) + + it('should call remoteTestEvents with force flag when overwriting', async () => { + const mockPutEvent = { + arn: 'arn:aws:lambda:us-west-2:123456789012:function:TestLambda', + name: 'ExistingEvent', + event: '{"key": "updated value"}', + region: 'us-west-2', + } + await remoteInvokeWebview.createRemoteTestEvents(mockPutEvent, true) // force = true + const expectedParams: SamCliRemoteTestEventsParameters = { + functionArn: mockPutEvent.arn, + operation: TestEventsOperation.Put, + name: mockPutEvent.name, + eventSample: mockPutEvent.event, + region: mockPutEvent.region, + force: true, // Should include force flag when overwriting + } + assert(runSamCliRemoteTestEventsStub.calledOnce, 'remoteTestEvents should be called once') + assert( + runSamCliRemoteTestEventsStub.calledWith(expectedParams), + 'remoteTestEvents should be called with force flag' + ) + }) }) describe('getRemoteTestEvents', () => { @@ -423,6 +827,56 @@ describe('RemoteInvokeWebview', () => { assert.strictEqual(result, undefined) }) }) + describe('tryOpenHandlerFile', () => { + let sandbox: sinon.SinonSandbox + let fsExistsStub: sinon.SinonStub + let getLambdaHandlerFileStub: sinon.SinonStub + + beforeEach(() => { + sandbox = sinon.createSandbox() + fsExistsStub = sandbox.stub(fs, 'exists') + getLambdaHandlerFileStub = sandbox.stub( + require('../../../../awsService/appBuilder/utils'), + 'getLambdaHandlerFile' + ) + }) + + afterEach(() => { + sandbox.restore() + }) + + it('should return false when LocalRootPath is not set', async () => { + const result = await remoteInvokeWebview.tryOpenHandlerFile() + assert.strictEqual(result, false) + }) + + it('should not watch for updates when LocalRootPath is already set (appbuilder case)', async () => { + const tempFolder = await makeTemporaryToolkitFolder() + const handlerPath = path.join(tempFolder, 'handler.js') + await fs.writeFile(handlerPath, 'exports.handler = () => {}') + + // Set LocalRootPath first to simulate appbuilder case + data.LocalRootPath = tempFolder + data.LambdaFunctionNode = { + configuration: { + Handler: 'handler.handler', + CodeSha256: 'abc123', + }, + } as any + data.Runtime = 'nodejs20.x' + + getLambdaHandlerFileStub.resolves(vscode.Uri.file(handlerPath)) + fsExistsStub.resolves(true) + + const result = await remoteInvokeWebview.tryOpenHandlerFile(tempFolder) + + assert.strictEqual(result, true) + // In appbuilder case, watchForUpdates should be false + + await fs.delete(tempFolder, { recursive: true }) + }) + }) + describe('invokeRemoteLambda', () => { let sandbox: sinon.SinonSandbox let outputChannel: vscode.OutputChannel diff --git a/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambdaDebugging.test.ts b/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambdaDebugging.test.ts index 04cce5f9cef..79f7863cd09 100644 --- a/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambdaDebugging.test.ts +++ b/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambdaDebugging.test.ts @@ -8,7 +8,8 @@ import { RemoteInvokeWebview, InitialData } from '../../../../lambda/vue/remoteI import { LambdaClient, DefaultLambdaClient } from '../../../../shared/clients/lambdaClient' import * as vscode from 'vscode' import sinon, { SinonStubbedInstance, createStubInstance } from 'sinon' -import { RemoteDebugController, DebugConfig } from '../../../../lambda/remoteDebugging/ldkController' +import { RemoteDebugController } from '../../../../lambda/remoteDebugging/ldkController' +import type { DebugConfig } from '../../../../lambda/remoteDebugging/lambdaDebugger' import { getTestWindow } from '../../../shared/vscode/window' import { LambdaFunctionNode } from '../../../../lambda/explorer/lambdaFunctionNode' import * as downloadLambda from '../../../../lambda/commands/downloadLambda' @@ -19,6 +20,7 @@ import globals from '../../../../shared/extensionGlobals' import fs from '../../../../shared/fs/fs' import { ToolkitError } from '../../../../shared' import { createMockDebugConfig } from '../../remoteDebugging/testUtils' +import { InvocationResponse } from '@aws-sdk/client-lambda' describe('RemoteInvokeWebview - Debugging Functionality', () => { let outputChannel: vscode.OutputChannel @@ -62,7 +64,7 @@ describe('RemoteInvokeWebview - Debugging Functionality', () => { regionSupportsRemoteDebug: true, } as InitialData - remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, data) + remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, client, data) // Mock RemoteDebugController mockDebugController = createStubInstance(RemoteDebugController) @@ -101,25 +103,6 @@ describe('RemoteInvokeWebview - Debugging Functionality', () => { remoteInvokeWebview.stopDebugTimer() assert.strictEqual(remoteInvokeWebview.getDebugTimeRemaining(), 0) }) - - it('should handle timer expiration by stopping debugging', async () => { - const stopDebuggingStub = sandbox.stub(remoteInvokeWebview, 'stopDebugging').resolves(true) - - // Mock a very short timer for testing - sandbox.stub(remoteInvokeWebview, 'startDebugTimer').callsFake(() => { - // Simulate immediate timer expiration - setTimeout(async () => { - await (remoteInvokeWebview as any).handleTimerExpired() - }, 10) - }) - - remoteInvokeWebview.startDebugTimer() - - // Wait for timer to expire - await new Promise((resolve) => setTimeout(resolve, 50)) - - assert(stopDebuggingStub.calledOnce, 'stopDebugging should be called when timer expires') - }) }) describe('Debug State Management', () => { @@ -184,7 +167,7 @@ describe('RemoteInvokeWebview - Debugging Functionality', () => { }) it('should return false when LambdaFunctionNode is undefined', async () => { - remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, { + remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, client, { ...data, LambdaFunctionNode: undefined, }) @@ -393,8 +376,8 @@ describe('RemoteInvokeWebview - Debugging Functionality', () => { it('should invoke lambda with remote debugging enabled', async () => { const mockResponse = { LogResult: Buffer.from('Debug log').toString('base64'), - Payload: '{"result": "debug success"}', - } + Payload: new TextEncoder().encode('{"result": "debug success"}'), + } satisfies InvocationResponse client.invoke.resolves(mockResponse) mockDebugController.isDebugging = true mockDebugController.qualifier = 'v1' @@ -410,8 +393,8 @@ describe('RemoteInvokeWebview - Debugging Functionality', () => { it('should handle timer management during debugging invocation', async () => { const mockResponse = { LogResult: Buffer.from('Debug log').toString('base64'), - Payload: '{"result": "debug success"}', - } + Payload: new TextEncoder().encode('{"result": "debug success"}'), + } satisfies InvocationResponse client.invoke.resolves(mockResponse) mockDebugController.isDebugging = true @@ -485,11 +468,14 @@ describe('RemoteInvokeWebview - Debugging Functionality', () => { functionArn: data.FunctionArn, functionName: data.FunctionName, }) - + async function mockRun(fn: (span: any) => T): Promise { + const span = { record: sandbox.stub() } + return fn(span) + } // Mock telemetry to avoid issues sandbox.stub(require('../../../../shared/telemetry/telemetry'), 'telemetry').value({ lambda_invokeRemote: { - emit: sandbox.stub(), + run: mockRun, }, }) }) @@ -516,8 +502,8 @@ describe('RemoteInvokeWebview - Debugging Functionality', () => { // 2. Test lambda invocation during debugging const mockResponse = { LogResult: Buffer.from('Debug invocation log').toString('base64'), - Payload: '{"debugResult": "success"}', - } + Payload: new TextEncoder().encode('{"debugResult": "success"}'), + } satisfies InvocationResponse client.invoke.resolves(mockResponse) await remoteInvokeWebview.invokeLambda('{"debugInput": "test"}', 'integration-test', true) @@ -577,8 +563,8 @@ describe('RemoteInvokeWebview - Debugging Functionality', () => { // Test invocation with version qualifier const mockResponse = { LogResult: Buffer.from('Version debug log').toString('base64'), - Payload: '{"versionResult": "success"}', - } + Payload: new TextEncoder().encode('{"versionResult": "success"}'), + } satisfies InvocationResponse client.invoke.resolves(mockResponse) await remoteInvokeWebview.invokeLambda('{"versionInput": "test"}', 'version-test', true) diff --git a/packages/core/src/test/lambda/vue/remoteInvoke/remoteInvoke.test.ts b/packages/core/src/test/lambda/vue/remoteInvoke/remoteInvoke.test.ts index d9f3f55fa92..9b5eab2e4df 100644 --- a/packages/core/src/test/lambda/vue/remoteInvoke/remoteInvoke.test.ts +++ b/packages/core/src/test/lambda/vue/remoteInvoke/remoteInvoke.test.ts @@ -8,65 +8,80 @@ import * as vscode from 'vscode' import * as samCliRemoteTestEvent from '../../../../shared/sam/cli/samCliRemoteTestEvent' import { TestEventsOperation } from '../../../../shared/sam/cli/samCliRemoteTestEvent' import sinon, { SinonStubbedInstance, createStubInstance } from 'sinon' -import { Lambda } from 'aws-sdk' +import { InvocationResponse } from '@aws-sdk/client-lambda' // Tests to check that the internal integration between the functions operates correctly describe('RemoteInvokeWebview', function () { let client: SinonStubbedInstance - let remoteInvokeWebview: RemoteInvokeWebview let outputChannel: vscode.OutputChannel - let mockData: any - before(async () => { - client = createStubInstance(DefaultLambdaClient) + const mockData = { + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:my-function', + } as any + const mockDataLMI = { + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:my-function', + LambdaFunctionNode: { + configuration: { + CapacityProviderConfig: { + blah: 'blah', + }, + }, + }, + } as any + const input = '{"key": "value"}' + const mockResponse = { + LogResult: Buffer.from('Test log').toString('base64'), + Payload: new TextEncoder().encode('{"result": "success"}'), + } satisfies InvocationResponse + + before(() => { outputChannel = { appendLine: (line: string) => {}, show: () => {}, } as vscode.OutputChannel - mockData = { - FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:my-function', - } - remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, mockData) }) - describe('Invoke Remote Lambda Function with Payload', () => { - it('should invoke with a simple payload', async function () { - const input = '{"key": "value"}' - const mockResponse: Lambda.InvocationResponse = { - LogResult: Buffer.from('Test log').toString('base64'), - Payload: '{"result": "success"}', - } - client.invoke.resolves(mockResponse) - await remoteInvokeWebview.invokeLambda(input) - sinon.assert.calledOnce(client.invoke) - sinon.assert.calledWith(client.invoke, mockData.FunctionArn, input) - }) + beforeEach(() => { + client = createStubInstance(DefaultLambdaClient) + }) + afterEach(() => { + sinon.restore() + }) + it('should invoke with a simple payload', async function () { + const remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, client, mockData) + client.invoke.resolves(mockResponse) + await remoteInvokeWebview.invokeLambda(input) + sinon.assert.calledOnce(client.invoke) + sinon.assert.calledWith(client.invoke, mockData.FunctionArn, input, undefined, 'Tail') }) - describe('Invoke Remote Lambda Function with Saved Events Payload', () => { - const mockEvent = { - name: 'TestEvent', - arn: 'arn:aws:lambda:us-west-2:123456789012:function:myFunction', - region: 'us-west-2', - } - const expectedParams = { - name: mockEvent.name, - operation: TestEventsOperation.Get, - functionArn: mockEvent.arn, - region: mockEvent.region, - } - const mockResponse = 'true' - let runSamCliRemoteTestEventsStub: sinon.SinonStub - beforeEach(() => { - runSamCliRemoteTestEventsStub = sinon.stub(samCliRemoteTestEvent, 'runSamCliRemoteTestEvents') - }) - afterEach(() => { - sinon.restore() - }) - it('should get saved event and invoke with it', async function () { - runSamCliRemoteTestEventsStub.resolves(mockResponse) - await remoteInvokeWebview.getRemoteTestEvents(mockEvent) - sinon.assert.calledOnce(runSamCliRemoteTestEventsStub) - sinon.assert.calledWith(runSamCliRemoteTestEventsStub, expectedParams) - }) + it('should invoke with no tail in LMI', async function () { + const remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, client, mockDataLMI) + client.invoke.resolves(mockResponse) + await remoteInvokeWebview.invokeLambda(input) + sinon.assert.calledOnce(client.invoke) + sinon.assert.calledWith(client.invoke, mockData.FunctionArn, input, undefined, 'None') + }) + + const mockEvent = { + name: 'TestEvent', + arn: 'arn:aws:lambda:us-west-2:123456789012:function:myFunction', + region: 'us-west-2', + } + const expectedParams = { + name: mockEvent.name, + operation: TestEventsOperation.Get, + functionArn: mockEvent.arn, + region: mockEvent.region, + } + const mockEventResponse = 'true' + + it('should get saved event and invoke with it', async function () { + const remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, client, mockData) + const runSamCliRemoteTestEventsStub = sinon.stub(samCliRemoteTestEvent, 'runSamCliRemoteTestEvents') + runSamCliRemoteTestEventsStub.resolves(mockEventResponse) + await remoteInvokeWebview.getRemoteTestEvents(mockEvent) + + sinon.assert.calledOnce(runSamCliRemoteTestEventsStub) + sinon.assert.calledWith(runSamCliRemoteTestEventsStub, expectedParams) }) }) diff --git a/packages/core/src/test/sagemakerunifiedstudio/activation.test.ts b/packages/core/src/test/sagemakerunifiedstudio/activation.test.ts new file mode 100644 index 00000000000..7c920771995 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/activation.test.ts @@ -0,0 +1,300 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import * as vscode from 'vscode' +import { activate } from '../../sagemakerunifiedstudio/activation' +import * as extensionUtilities from '../../shared/extensionUtilities' +import * as connectionMagicsSelectorActivation from '../../sagemakerunifiedstudio/connectionMagicsSelector/activation' +import * as explorerActivation from '../../sagemakerunifiedstudio/explorer/activation' +import * as resourceMetadataUtils from '../../sagemakerunifiedstudio/shared/utils/resourceMetadataUtils' +import * as setContext from '../../shared/vscode/setContext' +import { SmusUtils } from '../../sagemakerunifiedstudio/shared/smusUtils' +import * as smusUriHandlers from '../../sagemakerunifiedstudio/uriHandlers' +import { ExtContext } from '../../shared/extensions' + +describe('SageMaker Unified Studio Main Activation', function () { + let mockToolkitExtContext: ExtContext + let mockExtensionContext: vscode.ExtensionContext + let isSageMakerStub: sinon.SinonStub + let initializeResourceMetadataStub: sinon.SinonStub + let setContextStub: sinon.SinonStub + let isInSmusSpaceEnvironmentStub: sinon.SinonStub + let activateConnectionMagicsSelectorStub: sinon.SinonStub + let activateExplorerStub: sinon.SinonStub + let registerUriHandlerStub: sinon.SinonStub + + beforeEach(function () { + mockExtensionContext = { + subscriptions: [], + extensionPath: '/test/path', + globalState: { + get: sinon.stub(), + update: sinon.stub(), + }, + workspaceState: { + get: sinon.stub(), + update: sinon.stub(), + }, + } as any + + mockToolkitExtContext = { + extensionContext: mockExtensionContext, + awsContext: {} as any, + samCliContext: sinon.stub() as any, + regionProvider: {} as any, + outputChannel: {} as any, + telemetryService: {} as any, + uriHandler: {} as any, + credentialsStore: {} as any, + } + + // Stub all dependencies + isSageMakerStub = sinon.stub(extensionUtilities, 'isSageMaker') + initializeResourceMetadataStub = sinon.stub(resourceMetadataUtils, 'initializeResourceMetadata') + setContextStub = sinon.stub(setContext, 'setContext') + isInSmusSpaceEnvironmentStub = sinon.stub(SmusUtils, 'isInSmusSpaceEnvironment') + activateConnectionMagicsSelectorStub = sinon.stub(connectionMagicsSelectorActivation, 'activate') + activateExplorerStub = sinon.stub(explorerActivation, 'activate') + registerUriHandlerStub = sinon.stub(smusUriHandlers, 'register') + + // Set default return values + isSageMakerStub.returns(false) + initializeResourceMetadataStub.resolves() + setContextStub.resolves() + isInSmusSpaceEnvironmentStub.returns(false) + activateConnectionMagicsSelectorStub.resolves() + activateExplorerStub.resolves() + registerUriHandlerStub.returns({ dispose: sinon.stub() } as any) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('activate function', function () { + it('should always activate explorer regardless of environment', async function () { + isSageMakerStub.returns(false) + + await activate(mockToolkitExtContext) + + assert.ok(activateExplorerStub.calledOnceWith(mockExtensionContext)) + }) + + it('should not initialize SMUS components when not in SageMaker environment', async function () { + isSageMakerStub.returns(false) + + await activate(mockToolkitExtContext) + + assert.ok(initializeResourceMetadataStub.notCalled) + assert.ok(setContextStub.notCalled) + assert.ok(activateConnectionMagicsSelectorStub.notCalled) + assert.ok(activateExplorerStub.calledOnceWith(mockExtensionContext)) + }) + + it('should initialize SMUS components when in SMUS environment', async function () { + isSageMakerStub.withArgs('SMUS').returns(true) + isSageMakerStub.withArgs('SMUS-SPACE-REMOTE-ACCESS').returns(false) + isInSmusSpaceEnvironmentStub.returns(true) + + await activate(mockToolkitExtContext) + + assert.ok(initializeResourceMetadataStub.calledOnce) + assert.ok(setContextStub.calledOnceWith('aws.smus.inSmusSpaceEnvironment', true)) + assert.ok(activateConnectionMagicsSelectorStub.calledOnceWith(mockExtensionContext)) + assert.ok(activateExplorerStub.calledOnceWith(mockExtensionContext)) + }) + + it('should initialize SMUS components when in SMUS-SPACE-REMOTE-ACCESS environment', async function () { + isSageMakerStub.withArgs('SMUS').returns(false) + isSageMakerStub.withArgs('SMUS-SPACE-REMOTE-ACCESS').returns(true) + isInSmusSpaceEnvironmentStub.returns(false) + + await activate(mockToolkitExtContext) + + assert.ok(initializeResourceMetadataStub.calledOnce) + assert.ok(setContextStub.calledOnceWith('aws.smus.inSmusSpaceEnvironment', false)) + assert.ok(activateConnectionMagicsSelectorStub.calledOnceWith(mockExtensionContext)) + assert.ok(activateExplorerStub.calledOnceWith(mockExtensionContext)) + }) + + it('should call functions in correct order for SMUS environment', async function () { + isSageMakerStub.withArgs('SMUS').returns(true) + isSageMakerStub.withArgs('SMUS-SPACE-REMOTE-ACCESS').returns(false) + isInSmusSpaceEnvironmentStub.returns(true) + + await activate(mockToolkitExtContext) + + // Verify the order of calls + assert.ok(initializeResourceMetadataStub.calledBefore(setContextStub)) + assert.ok(setContextStub.calledBefore(activateConnectionMagicsSelectorStub)) + assert.ok(activateConnectionMagicsSelectorStub.calledBefore(activateExplorerStub)) + }) + + it('should handle initializeResourceMetadata errors', async function () { + isSageMakerStub.withArgs('SMUS').returns(true) + const error = new Error('Resource metadata initialization failed') + initializeResourceMetadataStub.rejects(error) + + await assert.rejects(() => activate(mockToolkitExtContext), /Resource metadata initialization failed/) + + assert.ok(initializeResourceMetadataStub.calledOnce) + assert.ok(setContextStub.notCalled) + assert.ok(activateConnectionMagicsSelectorStub.notCalled) + }) + + it('should handle setContext errors', async function () { + isSageMakerStub.withArgs('SMUS').returns(true) + isInSmusSpaceEnvironmentStub.returns(true) + const error = new Error('Set context failed') + setContextStub.rejects(error) + + await assert.rejects(() => activate(mockToolkitExtContext), /Set context failed/) + + assert.ok(initializeResourceMetadataStub.calledOnce) + assert.ok(setContextStub.calledOnce) + assert.ok(activateConnectionMagicsSelectorStub.notCalled) + }) + + it('should handle connectionMagicsSelector activation errors', async function () { + isSageMakerStub.withArgs('SMUS').returns(true) + isInSmusSpaceEnvironmentStub.returns(true) + const error = new Error('Connection magics selector activation failed') + activateConnectionMagicsSelectorStub.rejects(error) + + await assert.rejects(() => activate(mockToolkitExtContext), /Connection magics selector activation failed/) + + assert.ok(initializeResourceMetadataStub.calledOnce) + assert.ok(setContextStub.calledOnce) + assert.ok(activateConnectionMagicsSelectorStub.calledOnce) + }) + + it('should handle explorer activation errors', async function () { + const error = new Error('Explorer activation failed') + activateExplorerStub.rejects(error) + + await assert.rejects(() => activate(mockToolkitExtContext), /Explorer activation failed/) + + assert.ok(activateExplorerStub.calledOnce) + }) + + it('should pass correct extension context to all activation functions', async function () { + isSageMakerStub.withArgs('SMUS').returns(true) + isInSmusSpaceEnvironmentStub.returns(true) + + await activate(mockToolkitExtContext) + + assert.ok(activateConnectionMagicsSelectorStub.calledWith(mockExtensionContext)) + assert.ok(activateExplorerStub.calledWith(mockExtensionContext)) + }) + + it('should register URI handler', async function () { + await activate(mockToolkitExtContext) + + assert.ok(registerUriHandlerStub.calledOnceWith(mockToolkitExtContext)) + assert.ok(mockExtensionContext.subscriptions.length > 0) + }) + }) + + describe('environment detection logic', function () { + it('should check both SMUS and SMUS-SPACE-REMOTE-ACCESS environments', async function () { + isSageMakerStub.withArgs('SMUS').returns(false) + isSageMakerStub.withArgs('SMUS-SPACE-REMOTE-ACCESS').returns(false) + + await activate(mockToolkitExtContext) + + assert.ok(isSageMakerStub.calledWith('SMUS')) + assert.ok(isSageMakerStub.calledWith('SMUS-SPACE-REMOTE-ACCESS')) + }) + + it('should activate SMUS components if either environment check returns true', async function () { + // Test case 1: Only SMUS returns true + isSageMakerStub.withArgs('SMUS').returns(true) + isSageMakerStub.withArgs('SMUS-SPACE-REMOTE-ACCESS').returns(false) + isInSmusSpaceEnvironmentStub.returns(true) + + await activate(mockToolkitExtContext) + + assert.ok(initializeResourceMetadataStub.calledOnce) + assert.ok(activateConnectionMagicsSelectorStub.calledOnce) + + // Reset stubs for second test + initializeResourceMetadataStub.resetHistory() + activateConnectionMagicsSelectorStub.resetHistory() + + // Test case 2: Only SMUS-SPACE-REMOTE-ACCESS returns true + isSageMakerStub.withArgs('SMUS').returns(false) + isSageMakerStub.withArgs('SMUS-SPACE-REMOTE-ACCESS').returns(true) + isInSmusSpaceEnvironmentStub.returns(false) + + await activate(mockToolkitExtContext) + + assert.ok(initializeResourceMetadataStub.calledOnce) + assert.ok(activateConnectionMagicsSelectorStub.calledOnce) + }) + + it('should use SmusUtils.isInSmusSpaceEnvironment() result for context setting', async function () { + isSageMakerStub.withArgs('SMUS').returns(true) + + // Test with true + isInSmusSpaceEnvironmentStub.returns(true) + await activate(mockToolkitExtContext) + assert.ok(setContextStub.calledWith('aws.smus.inSmusSpaceEnvironment', true)) + + // Reset and test with false + setContextStub.resetHistory() + isInSmusSpaceEnvironmentStub.returns(false) + await activate(mockToolkitExtContext) + assert.ok(setContextStub.calledWith('aws.smus.inSmusSpaceEnvironment', false)) + }) + }) + + describe('integration scenarios', function () { + it('should handle mixed success and failure scenarios gracefully', async function () { + isSageMakerStub.withArgs('SMUS').returns(true) + isInSmusSpaceEnvironmentStub.returns(true) + + // initializeResourceMetadata succeeds, setContext fails + const setContextError = new Error('Context setting failed') + setContextStub.rejects(setContextError) + + await assert.rejects(() => activate(mockToolkitExtContext), /Context setting failed/) + + // Verify that initializeResourceMetadata was called but subsequent functions were not + assert.ok(initializeResourceMetadataStub.calledOnce) + assert.ok(setContextStub.calledOnce) + assert.ok(activateConnectionMagicsSelectorStub.notCalled) + assert.ok(activateExplorerStub.notCalled) + }) + + it('should complete successfully when all components initialize properly', async function () { + isSageMakerStub.withArgs('SMUS').returns(true) + isSageMakerStub.withArgs('SMUS-SPACE-REMOTE-ACCESS').returns(false) + isInSmusSpaceEnvironmentStub.returns(true) + + // All functions should succeed + await activate(mockToolkitExtContext) + + // Verify all expected functions were called + assert.ok(initializeResourceMetadataStub.calledOnce) + assert.ok(setContextStub.calledOnce) + assert.ok(activateConnectionMagicsSelectorStub.calledOnce) + assert.ok(activateExplorerStub.calledOnce) + assert.ok(registerUriHandlerStub.calledOnce) + }) + + it('should handle minimal extension context gracefully', async function () { + const minimalContext = { + extensionContext: mockExtensionContext, + } as any + + // Should not throw with minimal context + await activate(minimalContext) + + assert.ok(activateExplorerStub.called) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/auth/authenticationOrchestrator.test.ts b/packages/core/src/test/sagemakerunifiedstudio/auth/authenticationOrchestrator.test.ts new file mode 100644 index 00000000000..095922dfc6f --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/authenticationOrchestrator.test.ts @@ -0,0 +1,76 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { SmusAuthenticationOrchestrator } from '../../../sagemakerunifiedstudio/auth/authenticationOrchestrator' + +describe('SmusAuthenticationOrchestrator', function () { + // Note: Due to AWS Toolkit test framework restrictions on mocking vscode.window, + // these tests focus on the interface and behavior rather than deep mocking. + // The actual authentication flows are tested through integration tests. + + describe('handleIamAuthentication', function () { + it('should export the correct interface', function () { + // Verify the class exists and has the expected static method + assert.ok('handleIamAuthentication' in SmusAuthenticationOrchestrator) + assert.strictEqual(typeof SmusAuthenticationOrchestrator.handleIamAuthentication, 'function') + }) + + it('should be callable without throwing', function () { + // Verify the method exists and is accessible + assert.doesNotThrow(() => { + assert.ok('handleIamAuthentication' in SmusAuthenticationOrchestrator) + }) + }) + }) + + describe('handleSsoAuthentication', function () { + it('should export the correct interface', function () { + // Verify the class exists and has the expected static method + assert.ok('handleSsoAuthentication' in SmusAuthenticationOrchestrator) + assert.strictEqual(typeof SmusAuthenticationOrchestrator.handleSsoAuthentication, 'function') + }) + + it('should be callable without throwing', function () { + // Verify the method exists and is accessible + assert.doesNotThrow(() => { + assert.ok('handleSsoAuthentication' in SmusAuthenticationOrchestrator) + }) + }) + }) + + describe('return types', function () { + it('should handle SUCCESS and BACK return types correctly', function () { + // Test that the return types are properly defined for both methods + const testResult1: 'SUCCESS' | 'BACK' = 'SUCCESS' + const testResult2: 'SUCCESS' | 'BACK' = 'BACK' + + assert.strictEqual(testResult1, 'SUCCESS') + assert.strictEqual(testResult2, 'BACK') + }) + }) + + describe('class structure', function () { + it('should be a class with static methods', function () { + // Verify the orchestrator is properly structured + assert.strictEqual(typeof SmusAuthenticationOrchestrator, 'function') + assert.ok(SmusAuthenticationOrchestrator.prototype) + }) + + it('should have both required authentication methods', function () { + // Verify both authentication methods exist + const methods = ['handleIamAuthentication', 'handleSsoAuthentication'] + + for (const method of methods) { + assert.ok(method in SmusAuthenticationOrchestrator, `Missing method: ${method}`) + assert.strictEqual( + typeof SmusAuthenticationOrchestrator[method as keyof typeof SmusAuthenticationOrchestrator], + 'function', + `${method} should be a function` + ) + } + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/auth/connectionCredentialsProvider.test.ts b/packages/core/src/test/sagemakerunifiedstudio/auth/connectionCredentialsProvider.test.ts new file mode 100644 index 00000000000..8d14d21ba57 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/connectionCredentialsProvider.test.ts @@ -0,0 +1,223 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import { ConnectionCredentialsProvider } from '../../../sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider' +import { SmusAuthenticationProvider } from '../../../sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider' +import { DataZoneClient } from '../../../sagemakerunifiedstudio/shared/client/datazoneClient' +import { ToolkitError } from '../../../shared/errors' + +describe('ConnectionCredentialsProvider', function () { + let mockAuthProvider: sinon.SinonStubbedInstance + let mockDataZoneClient: sinon.SinonStubbedInstance + let connectionProvider: ConnectionCredentialsProvider + let dataZoneClientStub: sinon.SinonStub + + const testProjectId = 'proj-123456' + const testConnectionId = 'conn-123456' + const testDomainId = 'dzd_testdomain' + const testRegion = 'us-east-2' + + const mockConnectionCredentials = { + accessKeyId: 'AKIA-CONNECTION-KEY', + secretAccessKey: 'connection-secret-key', + sessionToken: 'connection-session-token', + expiration: new Date(Date.now() + 3600000), // 1 hour from now + } + + const mockGetConnectionResponse = { + connectionId: testConnectionId, + name: 'Test Connection', + type: 'S3', + domainId: testDomainId, + projectId: 'project-123', + connectionCredentials: mockConnectionCredentials, + } + + beforeEach(function () { + // Mock auth provider + mockAuthProvider = { + isConnected: sinon.stub().returns(true), + getDomainId: sinon.stub().returns(testDomainId), + getDomainRegion: sinon.stub().returns(testRegion), + getDerCredentialsProvider: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + activeConnection: { + ssoRegion: testRegion, + }, + } as any + + // Mock DataZone client + mockDataZoneClient = { + getConnection: sinon.stub().resolves(mockGetConnectionResponse), + } as any + + // Stub DataZoneClient.createWithCredentials + dataZoneClientStub = sinon.stub(DataZoneClient, 'createWithCredentials').returns(mockDataZoneClient as any) + + connectionProvider = new ConnectionCredentialsProvider(mockAuthProvider as any, testConnectionId, testProjectId) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('constructor', function () { + it('should create provider with correct properties', function () { + assert.strictEqual(connectionProvider.getConnectionId(), testConnectionId) + assert.strictEqual(connectionProvider.getDefaultRegion(), testRegion) + }) + }) + + describe('getCredentialsId', function () { + it('should return correct credentials ID', function () { + const credentialsId = connectionProvider.getCredentialsId() + assert.strictEqual(credentialsId.credentialSource, 'temp') + assert.strictEqual(credentialsId.credentialTypeId, `${testDomainId}:${testConnectionId}`) + }) + }) + + describe('getHashCode', function () { + it('should return correct hash code', function () { + const hashCode = connectionProvider.getHashCode() + assert.strictEqual(hashCode, `smus-connection:${testDomainId}:${testConnectionId}`) + }) + }) + + describe('isAvailable', function () { + it('should return true when auth provider is connected', async function () { + mockAuthProvider.isConnected.returns(true) + const isAvailable = await connectionProvider.isAvailable() + assert.strictEqual(isAvailable, true) + }) + + it('should return false when auth provider is not connected', async function () { + mockAuthProvider.isConnected.returns(false) + const isAvailable = await connectionProvider.isAvailable() + assert.strictEqual(isAvailable, false) + }) + + it('should return false when auth provider throws error', async function () { + mockAuthProvider.isConnected.throws(new Error('Connection error')) + const isAvailable = await connectionProvider.isAvailable() + assert.strictEqual(isAvailable, false) + }) + }) + + describe('canAutoConnect', function () { + it('should return false', async function () { + const canAutoConnect = await connectionProvider.canAutoConnect() + assert.strictEqual(canAutoConnect, false) + }) + }) + + describe('getCredentials', function () { + it('should fetch and return connection credentials', async function () { + const credentials = await connectionProvider.getCredentials() + + assert.strictEqual(credentials.accessKeyId, mockConnectionCredentials.accessKeyId) + assert.strictEqual(credentials.secretAccessKey, mockConnectionCredentials.secretAccessKey) + assert.strictEqual(credentials.sessionToken, mockConnectionCredentials.sessionToken) + assert(credentials.expiration instanceof Date) + + // Verify DataZone client was called correctly + sinon.assert.calledOnce(dataZoneClientStub) + sinon.assert.calledWith(mockDataZoneClient.getConnection, { + domainIdentifier: testDomainId, + identifier: testConnectionId, + withSecret: true, + }) + }) + + it('should use cached credentials on subsequent calls', async function () { + // First call + const credentials1 = await connectionProvider.getCredentials() + // Second call + const credentials2 = await connectionProvider.getCredentials() + + assert.strictEqual(credentials1, credentials2) + // DataZone client should only be called once due to caching + sinon.assert.calledOnce(mockDataZoneClient.getConnection) + }) + + it('should throw error when no connection credentials available', async function () { + mockDataZoneClient.getConnection.resolves({ + ...mockGetConnectionResponse, + connectionCredentials: undefined, + }) + + await assert.rejects( + () => connectionProvider.getCredentials(), + (err: ToolkitError) => { + assert.strictEqual(err.code, 'NoConnectionCredentials') + return true + } + ) + }) + + it('should throw error when connection credentials are invalid', async function () { + mockDataZoneClient.getConnection.resolves({ + ...mockGetConnectionResponse, + connectionCredentials: { + accessKeyId: '', // Invalid empty string + secretAccessKey: 'valid-secret', + sessionToken: 'valid-token', + }, + }) + + await assert.rejects( + () => connectionProvider.getCredentials(), + (err: ToolkitError) => { + assert.strictEqual(err.code, 'InvalidConnectionCredentials') + return true + } + ) + }) + + it('should throw error when DataZone client fails', async function () { + const dataZoneError = new Error('DataZone API error') + mockDataZoneClient.getConnection.rejects(dataZoneError) + + await assert.rejects( + () => connectionProvider.getCredentials(), + (err: ToolkitError) => { + assert.strictEqual(err.code, 'ConnectionCredentialsFetchFailed') + return true + } + ) + }) + }) + + describe('invalidate', function () { + it('should clear cached credentials', async function () { + // Get credentials to populate cache + await connectionProvider.getCredentials() + sinon.assert.calledOnce(mockDataZoneClient.getConnection) + + // Invalidate cache + connectionProvider.invalidate() + + // Get credentials again - should make new API call + await connectionProvider.getCredentials() + sinon.assert.calledTwice(mockDataZoneClient.getConnection) + }) + }) + + describe('provider metadata', function () { + it('should return correct provider type', function () { + assert.strictEqual(connectionProvider.getProviderType(), 'temp') + }) + + it('should return correct telemetry type', function () { + assert.strictEqual(connectionProvider.getTelemetryType(), 'other') + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/auth/domainExecRoleCredentialsProvider.test.ts b/packages/core/src/test/sagemakerunifiedstudio/auth/domainExecRoleCredentialsProvider.test.ts new file mode 100644 index 00000000000..7e8cdd8632d --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/domainExecRoleCredentialsProvider.test.ts @@ -0,0 +1,583 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import { DomainExecRoleCredentialsProvider } from '../../../sagemakerunifiedstudio/auth/providers/domainExecRoleCredentialsProvider' +import { ToolkitError } from '../../../shared/errors' +import fetch from 'node-fetch' +import { SmusTimeouts } from '../../../sagemakerunifiedstudio/shared/smusUtils' + +describe('DomainExecRoleCredentialsProvider', function () { + let derProvider: DomainExecRoleCredentialsProvider + let mockGetAccessToken: sinon.SinonStub + let fetchStub: sinon.SinonStub + + const testDomainId = 'dzd_testdomain' + const testDomainUrl = 'https://test-domain.sagemaker.us-east-2.on.aws' + const testSsoRegion = 'us-east-2' + const testAccessToken = 'test-access-token-12345' + + const mockCredentialsResponse = { + credentials: { + accessKeyId: 'AKIA-DER-KEY', + secretAccessKey: 'der-secret-key', + sessionToken: 'der-session-token', + }, + } + + beforeEach(function () { + // Mock access token function + mockGetAccessToken = sinon.stub().resolves(testAccessToken) + + // Mock fetch + fetchStub = sinon.stub(fetch, 'default' as any).resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(mockCredentialsResponse)), + json: sinon.stub().resolves(mockCredentialsResponse), + } as any) + + derProvider = new DomainExecRoleCredentialsProvider( + testDomainUrl, + testDomainId, + testSsoRegion, + mockGetAccessToken + ) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('constructor', function () { + it('should initialize with correct properties', function () { + assert.strictEqual(derProvider.getDomainId(), testDomainId) + assert.strictEqual(derProvider.getDomainUrl(), testDomainUrl) + assert.strictEqual(derProvider.getDefaultRegion(), testSsoRegion) + }) + }) + + describe('getCredentialsId', function () { + it('should return correct credentials ID', function () { + const credentialsId = derProvider.getCredentialsId() + assert.strictEqual(credentialsId.credentialSource, 'sso') + assert.strictEqual(credentialsId.credentialTypeId, testDomainId) + }) + }) + + describe('getProviderType', function () { + it('should return sso provider type', function () { + assert.strictEqual(derProvider.getProviderType(), 'sso') + }) + }) + + describe('getTelemetryType', function () { + it('should return ssoProfile telemetry type', function () { + assert.strictEqual(derProvider.getTelemetryType(), 'ssoProfile') + }) + }) + + describe('getHashCode', function () { + it('should return correct hash code', function () { + const hashCode = derProvider.getHashCode() + assert.strictEqual(hashCode, `smus-der:${testDomainId}:${testSsoRegion}`) + }) + }) + + describe('canAutoConnect', function () { + it('should return false', async function () { + const result = await derProvider.canAutoConnect() + assert.strictEqual(result, false) + }) + }) + + describe('isAvailable', function () { + it('should return true when access token is available', async function () { + const result = await derProvider.isAvailable() + assert.strictEqual(result, true) + assert.ok(mockGetAccessToken.called) + }) + + it('should return false when access token throws error', async function () { + mockGetAccessToken.rejects(new Error('Token error')) + const result = await derProvider.isAvailable() + assert.strictEqual(result, false) + }) + }) + + describe('getCredentials', function () { + it('should fetch and cache DER credentials', async function () { + const credentials = await derProvider.getCredentials() + + // Verify access token was fetched + assert.ok(mockGetAccessToken.called) + + // Verify fetch was called with correct parameters + assert.ok(fetchStub.called) + const fetchCall = fetchStub.firstCall + assert.strictEqual(fetchCall.args[0], `${testDomainUrl}/sso/redeem-token`) + + const fetchOptions = fetchCall.args[1] + assert.strictEqual(fetchOptions.method, 'POST') + assert.strictEqual(fetchOptions.headers['Content-Type'], 'application/json') + assert.strictEqual(fetchOptions.headers['Accept'], 'application/json') + assert.strictEqual(fetchOptions.headers['User-Agent'], 'aws-toolkit-vscode') + + const requestBody = JSON.parse(fetchOptions.body) + assert.strictEqual(requestBody.domainId, testDomainId) + assert.strictEqual(requestBody.accessToken, testAccessToken) + + // Verify timeout is set + assert.strictEqual(fetchOptions.timeout, SmusTimeouts.apiCallTimeoutMs) + assert.strictEqual(fetchOptions.timeout, 10000) // 10 seconds + + // Verify returned credentials + assert.strictEqual(credentials.accessKeyId, mockCredentialsResponse.credentials.accessKeyId) + assert.strictEqual(credentials.secretAccessKey, mockCredentialsResponse.credentials.secretAccessKey) + assert.strictEqual(credentials.sessionToken, mockCredentialsResponse.credentials.sessionToken) + assert.ok(credentials.expiration) + }) + + it('should use cached credentials when available', async function () { + // First call should fetch credentials + const credentials1 = await derProvider.getCredentials() + + // Second call should use cache + const credentials2 = await derProvider.getCredentials() + + // Fetch should only be called once + assert.strictEqual(fetchStub.callCount, 1) + assert.strictEqual(mockGetAccessToken.callCount, 1) + + // Credentials should be the same + assert.strictEqual(credentials1, credentials2) + }) + + it('should handle missing access token', async function () { + mockGetAccessToken.resolves('') + + await assert.rejects( + () => derProvider.getCredentials(), + (err: ToolkitError) => { + return err.code === 'DerCredentialsFetchFailed' && err.message.includes('No access token available') + } + ) + }) + + it('should handle HTTP errors from redeem token API', async function () { + fetchStub.resolves({ + ok: false, + status: 401, + statusText: 'Unauthorized', + text: sinon.stub().resolves('Invalid token'), + } as any) + + await assert.rejects( + () => derProvider.getCredentials(), + (err: ToolkitError) => { + return err.code === 'DerCredentialsFetchFailed' && err.message.includes('401') + } + ) + }) + + it('should handle timeout errors', async function () { + const timeoutError = new Error('Request timeout') + timeoutError.name = 'AbortError' + fetchStub.rejects(timeoutError) + + await assert.rejects( + () => derProvider.getCredentials(), + (err: ToolkitError) => { + return ( + err.code === 'DerCredentialsFetchFailed' && err.message.includes('timed out after 10 seconds') + ) + } + ) + }) + + it('should handle network errors', async function () { + const networkError = new Error('Network error') + fetchStub.rejects(networkError) + + await assert.rejects( + () => derProvider.getCredentials(), + (err: ToolkitError) => { + return err.code === 'DerCredentialsFetchFailed' + } + ) + }) + + it('should handle missing credentials object in response', async function () { + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify({})), + json: sinon.stub().resolves({}), // Missing credentials object + } as any) + + await assert.rejects( + () => derProvider.getCredentials(), + (err: ToolkitError) => { + return ( + err.code === 'DerCredentialsFetchFailed' && err.message.includes('Missing credentials object') + ) + } + ) + }) + + it('should handle invalid accessKeyId in response', async function () { + const invalidResponse = { + credentials: { + accessKeyId: '', // Invalid empty string + secretAccessKey: 'valid-secret', + sessionToken: 'valid-token', + }, + } + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(invalidResponse)), + json: sinon.stub().resolves(invalidResponse), + } as any) + + await assert.rejects( + () => derProvider.getCredentials(), + (err: ToolkitError) => { + return err.code === 'DerCredentialsFetchFailed' && err.message.includes('Invalid accessKeyId') + } + ) + }) + + it('should handle invalid secretAccessKey in response', async function () { + const invalidResponse = { + credentials: { + accessKeyId: 'valid-key', + secretAccessKey: undefined, // Invalid null value + sessionToken: 'valid-token', + }, + } + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(invalidResponse)), + json: sinon.stub().resolves(invalidResponse), + } as any) + + await assert.rejects( + () => derProvider.getCredentials(), + (err: ToolkitError) => { + return err.code === 'DerCredentialsFetchFailed' && err.message.includes('Invalid secretAccessKey') + } + ) + }) + + it('should handle invalid sessionToken in response', async function () { + const invalidResponse = { + credentials: { + accessKeyId: 'valid-key', + secretAccessKey: 'valid-secret', + sessionToken: undefined, // Invalid undefined value + }, + } + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(invalidResponse)), + json: sinon.stub().resolves(invalidResponse), + } as any) + + await assert.rejects( + () => derProvider.getCredentials(), + (err: ToolkitError) => { + return err.code === 'DerCredentialsFetchFailed' && err.message.includes('Invalid sessionToken') + } + ) + }) + + it('should set default expiration when not provided in response', async function () { + const credentials = await derProvider.getCredentials() + + // Should have expiration set to 10 mins from now + assert.ok(credentials.expiration) + const expirationTime = credentials.expiration!.getTime() + const expectedTime = Date.now() + 10 * 60 * 1000 // 10 minutes + const timeDiff = Math.abs(expirationTime - expectedTime) + assert.ok(timeDiff < 5000, 'Expiration should be 10 mins from now') + }) + + it('should use expiration from API response when provided as ISO string', async function () { + const futureExpiration = new Date(Date.now() + 2 * 60 * 60 * 1000) // 2 hours from now + const responseWithExpiration = { + credentials: { + accessKeyId: 'AKIA-DER-KEY', + secretAccessKey: 'der-secret-key', + sessionToken: 'der-session-token', + expiration: futureExpiration.toISOString(), // API returns expiration as ISO string + }, + } + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(responseWithExpiration)), + json: sinon.stub().resolves(responseWithExpiration), + } as any) + + const credentials = await derProvider.getCredentials() + + // Should use the expiration from the API response + assert.ok(credentials.expiration) + const expirationTime = credentials.expiration!.getTime() + const expectedTime = futureExpiration.getTime() + const timeDiff = Math.abs(expirationTime - expectedTime) + assert.ok(timeDiff < 1000, 'Should use expiration from API response') + }) + + it('should handle epoch timestamp in seconds from API response', async function () { + const futureTime = Math.floor(Date.now() / 1000) + 3600 // 1 hour from now in seconds + const responseWithEpochExpiration = { + credentials: { + accessKeyId: 'AKIA-DER-KEY', + secretAccessKey: 'der-secret-key', + sessionToken: 'der-session-token', + expiration: futureTime.toString(), // Epoch timestamp in seconds as string + }, + } + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(responseWithEpochExpiration)), + json: sinon.stub().resolves(responseWithEpochExpiration), + } as any) + + const credentials = await derProvider.getCredentials() + + // Should correctly parse epoch timestamp and convert to Date + assert.ok(credentials.expiration) + const expirationTime = credentials.expiration!.getTime() + const expectedTime = futureTime * 1000 // Convert to milliseconds + const timeDiff = Math.abs(expirationTime - expectedTime) + assert.ok(timeDiff < 1000, 'Should correctly parse epoch timestamp in seconds') + }) + + it('should handle epoch timestamp as number from API response', async function () { + const futureTime = Math.floor(Date.now() / 1000) + 7200 // 2 hours from now in seconds + const responseWithEpochExpiration = { + credentials: { + accessKeyId: 'AKIA-DER-KEY', + secretAccessKey: 'der-secret-key', + sessionToken: 'der-session-token', + expiration: futureTime, // Epoch timestamp in seconds as number + }, + } + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(responseWithEpochExpiration)), + json: sinon.stub().resolves(responseWithEpochExpiration), + } as any) + + const credentials = await derProvider.getCredentials() + + // Should correctly parse epoch timestamp and convert to Date + assert.ok(credentials.expiration) + const expirationTime = credentials.expiration!.getTime() + const expectedTime = futureTime * 1000 // Convert to milliseconds + const timeDiff = Math.abs(expirationTime - expectedTime) + assert.ok(timeDiff < 1000, 'Should correctly parse epoch timestamp as number') + }) + + it('should handle zero epoch timestamp gracefully', async function () { + const responseWithZeroExpiration = { + credentials: { + accessKeyId: 'AKIA-DER-KEY', + secretAccessKey: 'der-secret-key', + sessionToken: 'der-session-token', + expiration: '0', // Zero is not > 0, so treated as ISO string "0" which represents year 0 + }, + } + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(responseWithZeroExpiration)), + json: sinon.stub().resolves(responseWithZeroExpiration), + } as any) + + const credentials = await derProvider.getCredentials() + + // "0" is parsed as a valid date (year 0), not as an invalid date + // So it should use the parsed date, not the default expiration + assert.ok(credentials.expiration) + const expirationTime = credentials.expiration!.getTime() + const expectedTime = new Date('0').getTime() // Year 0 + assert.strictEqual(expirationTime, expectedTime, 'Should use parsed date for year 0') + }) + + it('should handle negative epoch timestamp gracefully', async function () { + const responseWithNegativeExpiration = { + credentials: { + accessKeyId: 'AKIA-DER-KEY', + secretAccessKey: 'der-secret-key', + sessionToken: 'der-session-token', + expiration: '-1', // Negative is not > 0, so treated as ISO string "-1" which represents year -1 + }, + } + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(responseWithNegativeExpiration)), + json: sinon.stub().resolves(responseWithNegativeExpiration), + } as any) + + const credentials = await derProvider.getCredentials() + + // "-1" is parsed as a valid date (year -1), not as an invalid date + // So it should use the parsed date, not the default expiration + assert.ok(credentials.expiration) + const expirationTime = credentials.expiration!.getTime() + const expectedTime = new Date('-1').getTime() // Year -1 + assert.strictEqual(expirationTime, expectedTime, 'Should use parsed date for year -1') + }) + + it('should handle JSON parsing errors', async function () { + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves('invalid json'), + json: sinon.stub().rejects(new Error('Invalid JSON')), + } as any) + + await assert.rejects( + () => derProvider.getCredentials(), + (err: ToolkitError) => { + return err.code === 'DerCredentialsFetchFailed' + } + ) + }) + + it('should handle invalid expiration string in response', async function () { + const responseWithInvalidExpiration = { + credentials: { + accessKeyId: 'AKIA-DER-KEY', + secretAccessKey: 'der-secret-key', + sessionToken: 'der-session-token', + expiration: 'invalid-date-string', // Invalid date string + }, + } + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(responseWithInvalidExpiration)), + json: sinon.stub().resolves(responseWithInvalidExpiration), + } as any) + + const credentials = await derProvider.getCredentials() + + // Should fall back to default expiration when date parsing fails + assert.ok(credentials.expiration) + const expirationTime = credentials.expiration!.getTime() + + // Should be a valid timestamp (not NaN) using the default expiration + assert.ok(!isNaN(expirationTime), 'Should have valid expiration timestamp') + + // Should be close to now + 10 minutes (default expiration) + const expectedTime = Date.now() + 10 * 60 * 1000 + const timeDiff = Math.abs(expirationTime - expectedTime) + assert.ok(timeDiff < 5000, 'Should fall back to default expiration for invalid date string') + }) + + it('should handle empty expiration string in response', async function () { + const responseWithEmptyExpiration = { + credentials: { + accessKeyId: 'AKIA-DER-KEY', + secretAccessKey: 'der-secret-key', + sessionToken: 'der-session-token', + expiration: '', // Empty string + }, + } + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(responseWithEmptyExpiration)), + json: sinon.stub().resolves(responseWithEmptyExpiration), + } as any) + + const credentials = await derProvider.getCredentials() + + // Should fall back to default expiration for empty string + assert.ok(credentials.expiration) + const expirationTime = credentials.expiration!.getTime() + const expectedTime = Date.now() + 10 * 60 * 1000 // Default 10 minutes + const timeDiff = Math.abs(expirationTime - expectedTime) + assert.ok(timeDiff < 5000, 'Should use default expiration for empty string') + }) + + it('should handle non-numeric string that looks like a number', async function () { + const responseWithInvalidNumber = { + credentials: { + accessKeyId: 'AKIA-DER-KEY', + secretAccessKey: 'der-secret-key', + sessionToken: 'der-session-token', + expiration: '123abc', // Non-numeric string + }, + } + + fetchStub.resolves({ + ok: true, + status: 200, + statusText: 'OK', + text: sinon.stub().resolves(JSON.stringify(responseWithInvalidNumber)), + json: sinon.stub().resolves(responseWithInvalidNumber), + } as any) + + const credentials = await derProvider.getCredentials() + + // Should fall back to default expiration for invalid numeric string + assert.ok(credentials.expiration) + const expirationTime = credentials.expiration!.getTime() + const expectedTime = Date.now() + 10 * 60 * 1000 // Default 10 minutes + const timeDiff = Math.abs(expirationTime - expectedTime) + assert.ok(timeDiff < 5000, 'Should use default expiration for invalid numeric string') + }) + }) + + describe('invalidate', function () { + it('should clear cache and force fresh fetch on next call', async function () { + // First call to populate cache + await derProvider.getCredentials() + assert.strictEqual(fetchStub.callCount, 1) + + // Invalidate should clear cache + derProvider.invalidate() + + // Next call should fetch fresh credentials + await derProvider.getCredentials() + assert.strictEqual(fetchStub.callCount, 2) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/auth/model.test.ts b/packages/core/src/test/sagemakerunifiedstudio/auth/model.test.ts new file mode 100644 index 00000000000..e1d030ea825 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/model.test.ts @@ -0,0 +1,345 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { Credentials } from '@aws-sdk/types' +import * as sinon from 'sinon' +import { + SmusSsoConnection, + SmusIamConnection, + isSmusIamConnection, + isSmusSsoConnection, + isValidSmusConnection, + createSmusProfile, + scopeSmus, + getDataZoneSsoScope, +} from '../../../sagemakerunifiedstudio/auth/model' +import { DevSettings } from '../../../shared/settings' + +describe('SMUS Connection Model', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + const mockCredentials: Credentials = { + accessKeyId: 'test-access-key', + secretAccessKey: 'test-secret-key', + } + + const mockCredentialsProvider = async (): Promise => mockCredentials + + const mockGetToken = async () => ({ + accessToken: 'mock-access-token', + expiresAt: new Date(Date.now() + 3600000), // 1 hour from now + }) + + const mockGetRegistration = async () => ({ + clientId: 'mock-client-id', + clientSecret: 'mock-client-secret', + expiresAt: new Date(Date.now() + 86400000), // 24 hours from now + startUrl: 'https://test.sagemaker.us-east-1.on.aws/', + }) + + describe('isSmusIamConnection', function () { + it('should return true for valid SMUS IAM connection', function () { + const connection: SmusIamConnection = { + type: 'iam', + profileName: 'test-profile', + region: 'us-east-1', + domainUrl: 'https://test.sagemaker.us-east-1.on.aws/', + domainId: 'test-domain-id', + id: 'test-id', + label: 'Test IAM Connection', + endpointUrl: undefined, + getCredentials: mockCredentialsProvider, + } + + assert.strictEqual(isSmusIamConnection(connection), true) + }) + + it('should return false for SSO connection', function () { + const connection: SmusSsoConnection = { + type: 'sso', + startUrl: 'https://test.awsapps.com/start', + ssoRegion: 'us-east-1', + scopes: [scopeSmus], + domainUrl: 'https://test.sagemaker.us-east-1.on.aws/', + domainId: 'test-domain-id', + id: 'test-id', + label: 'Test SSO Connection', + getToken: mockGetToken, + getRegistration: mockGetRegistration, + } + + assert.strictEqual(isSmusIamConnection(connection), false) + }) + + it('should return false for connection missing required IAM properties', function () { + const connection = { + type: 'iam', + profileName: 'test-profile', + // Missing region, domainUrl, domainId, getCredentials + id: 'test-id', + label: 'Test IAM Connection', + endpointUrl: undefined, + } + + assert.strictEqual(isSmusIamConnection(connection as any), false) + }) + + it('should return false for undefined connection', function () { + assert.strictEqual(isSmusIamConnection(undefined), false) + }) + + it('should return false for connection with wrong type', function () { + const connection = { + type: 'other', + profileName: 'test-profile', + region: 'us-east-1', + domainUrl: 'https://test.sagemaker.us-east-1.on.aws/', + domainId: 'test-domain-id', + id: 'test-id', + label: 'Test Connection', + } + + assert.strictEqual(isSmusIamConnection(connection as any), false) + }) + }) + + describe('isSmusSsoConnection', function () { + it('should return true for valid SMUS SSO connection', function () { + const connection: SmusSsoConnection = { + type: 'sso', + startUrl: 'https://test.awsapps.com/start', + ssoRegion: 'us-east-1', + scopes: [scopeSmus], + domainUrl: 'https://test.sagemaker.us-east-1.on.aws/', + domainId: 'test-domain-id', + id: 'test-id', + label: 'Test SSO Connection', + getToken: mockGetToken, + getRegistration: mockGetRegistration, + } + + assert.strictEqual(isSmusSsoConnection(connection), true) + }) + + it('should return false for IAM connection', function () { + const connection: SmusIamConnection = { + type: 'iam', + profileName: 'test-profile', + region: 'us-east-1', + domainUrl: 'https://test.sagemaker.us-east-1.on.aws/', + domainId: 'test-domain-id', + id: 'test-id', + label: 'Test IAM Connection', + endpointUrl: undefined, + getCredentials: mockCredentialsProvider, + } + + assert.strictEqual(isSmusSsoConnection(connection), false) + }) + + it('should return false for SSO connection without SMUS scope', function () { + const connection = { + type: 'sso', + startUrl: 'https://test.awsapps.com/start', + ssoRegion: 'us-east-1', + scopes: ['other:scope'], + domainUrl: 'https://test.sagemaker.us-east-1.on.aws/', + domainId: 'test-domain-id', + id: 'test-id', + label: 'Test SSO Connection', + } + + assert.strictEqual(isSmusSsoConnection(connection as any), false) + }) + + it('should return false for SSO connection missing SMUS properties', function () { + const connection = { + type: 'sso', + startUrl: 'https://test.awsapps.com/start', + ssoRegion: 'us-east-1', + scopes: [scopeSmus], + // Missing domainUrl and domainId + id: 'test-id', + label: 'Test SSO Connection', + } + + assert.strictEqual(isSmusSsoConnection(connection as any), false) + }) + + it('should return false for undefined connection', function () { + assert.strictEqual(isSmusSsoConnection(undefined), false) + }) + }) + + describe('isValidSmusConnection', function () { + it('should return true for valid SMUS SSO connection', function () { + const connection: SmusSsoConnection = { + type: 'sso', + startUrl: 'https://test.awsapps.com/start', + ssoRegion: 'us-east-1', + scopes: [scopeSmus], + domainUrl: 'https://test.sagemaker.us-east-1.on.aws/', + domainId: 'test-domain-id', + id: 'test-id', + label: 'Test SSO Connection', + getToken: mockGetToken, + getRegistration: mockGetRegistration, + } + + assert.strictEqual(isValidSmusConnection(connection), true) + }) + + it('should return true for valid SMUS IAM connection', function () { + const connection: SmusIamConnection = { + type: 'iam', + profileName: 'test-profile', + region: 'us-east-1', + domainUrl: 'https://test.sagemaker.us-east-1.on.aws/', + domainId: 'test-domain-id', + id: 'test-id', + label: 'Test IAM Connection', + endpointUrl: undefined, + getCredentials: mockCredentialsProvider, + } + + assert.strictEqual(isValidSmusConnection(connection), true) + }) + + it('should return false for invalid connection', function () { + const connection = { + type: 'other', + id: 'test-id', + label: 'Test Connection', + } + + assert.strictEqual(isValidSmusConnection(connection), false) + }) + + it('should return false for undefined connection', function () { + assert.strictEqual(isValidSmusConnection(undefined), false) + }) + }) + + describe('getDataZoneSsoScope', function () { + it('should return default scope when no custom setting is provided', function () { + // When get() is called with default value, it returns the default (scopeSmus) + // This simulates the behavior when aws.dev.datazoneScope is not set + sandbox.stub(DevSettings.instance, 'get').withArgs('datazoneScope', scopeSmus).returns(scopeSmus) + + const scope = getDataZoneSsoScope() + + assert.strictEqual(scope, scopeSmus) + }) + + it('should return custom scope when setting is configured', function () { + const customScope = 'custom:datazone:scope' + // When get() is called, it returns the custom value from settings + // This simulates the behavior when aws.dev.datazoneScope is set to customScope + sandbox.stub(DevSettings.instance, 'get').withArgs('datazoneScope', scopeSmus).returns(customScope) + + const scope = getDataZoneSsoScope() + + assert.strictEqual(scope, customScope) + }) + }) + + describe('createSmusProfile', function () { + it('should create a valid SMUS profile with default scope', function () { + sandbox.stub(DevSettings.instance, 'get').withArgs('datazoneScope', scopeSmus).returns(scopeSmus) + + const domainUrl = 'https://test.sagemaker.us-east-1.on.aws/' + const domainId = 'test-domain-id' + const startUrl = 'https://test.awsapps.com/start' + const region = 'us-east-1' + + const profile = createSmusProfile(domainUrl, domainId, startUrl, region) + + assert.strictEqual(profile.domainUrl, domainUrl) + assert.strictEqual(profile.domainId, domainId) + assert.strictEqual(profile.startUrl, startUrl) + assert.strictEqual(profile.ssoRegion, region) + assert.strictEqual(profile.type, 'sso') + assert.deepStrictEqual(profile.scopes, [scopeSmus]) + }) + + it('should create a valid SMUS profile with custom scope from settings', function () { + const customScope = 'custom:datazone:scope' + sandbox.stub(DevSettings.instance, 'get').withArgs('datazoneScope', scopeSmus).returns(customScope) + + const domainUrl = 'https://test.sagemaker.us-east-1.on.aws/' + const domainId = 'test-domain-id' + const startUrl = 'https://test.awsapps.com/start' + const region = 'us-east-1' + + const profile = createSmusProfile(domainUrl, domainId, startUrl, region) + + assert.deepStrictEqual(profile.scopes, [customScope]) + }) + + it('should create a valid SMUS profile with custom scopes parameter', function () { + const domainUrl = 'https://test.sagemaker.us-east-1.on.aws/' + const domainId = 'test-domain-id' + const startUrl = 'https://test.awsapps.com/start' + const region = 'us-east-1' + const customScopes = ['custom:scope1', 'custom:scope2'] + + const profile = createSmusProfile(domainUrl, domainId, startUrl, region, customScopes) + + assert.deepStrictEqual(profile.scopes, customScopes) + }) + }) + + describe('isSmusSsoConnection with custom scope', function () { + it('should return true for connection with custom scope from settings', function () { + const customScope = 'custom:datazone:scope' + sandbox.stub(DevSettings.instance, 'get').withArgs('datazoneScope', scopeSmus).returns(customScope) + + const connection = { + type: 'sso', + startUrl: 'https://test.awsapps.com/start', + ssoRegion: 'us-east-1', + scopes: [customScope], + domainUrl: 'https://test.sagemaker.us-east-1.on.aws/', + domainId: 'test-domain-id', + id: 'test-id', + label: 'Test SSO Connection', + getToken: mockGetToken, + getRegistration: mockGetRegistration, + } as SmusSsoConnection + + assert.strictEqual(isSmusSsoConnection(connection), true) + }) + + it('should return true for connection with default scope even when custom scope is configured', function () { + const customScope = 'custom:datazone:scope' + sandbox.stub(DevSettings.instance, 'get').withArgs('datazoneScope', scopeSmus).returns(customScope) + + const connection = { + type: 'sso', + startUrl: 'https://test.awsapps.com/start', + ssoRegion: 'us-east-1', + scopes: [scopeSmus], // Using default scope + domainUrl: 'https://test.sagemaker.us-east-1.on.aws/', + domainId: 'test-domain-id', + id: 'test-id', + label: 'Test SSO Connection', + getToken: mockGetToken, + getRegistration: mockGetRegistration, + } as SmusSsoConnection + + // Should still work for backward compatibility + assert.strictEqual(isSmusSsoConnection(connection), true) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/auth/preferences/authenticationPreferences.test.ts b/packages/core/src/test/sagemakerunifiedstudio/auth/preferences/authenticationPreferences.test.ts new file mode 100644 index 00000000000..06549830934 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/preferences/authenticationPreferences.test.ts @@ -0,0 +1,303 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { + SmusAuthenticationPreferencesManager, + SmusAuthenticationPreferences, + SmusIamProfileConfig, +} from '../../../../sagemakerunifiedstudio/auth/preferences/authenticationPreferences' +import { globals } from '../../../../shared' + +describe('SmusAuthenticationPreferencesManager', function () { + let mockContext: any + let sandbox: sinon.SinonSandbox + let mockGlobalState: any + + beforeEach(function () { + sandbox = sinon.createSandbox() + + // Mock the globals.globalState instead of context.globalState directly + mockGlobalState = { + get: sandbox.stub(), + update: sandbox.stub().resolves(), + } + + // Mock VS Code extension context (still needed for the API) + mockContext = { + globalState: mockGlobalState, + } + + // Stub globals.globalState to use our mock + sandbox.stub(globals, 'globalState').value(mockGlobalState) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('getPreferences', function () { + it('should return default preferences when none are stored', function () { + // Setup + mockGlobalState.get.returns(undefined) + + // Act + const preferences = SmusAuthenticationPreferencesManager.getPreferences(mockContext) + + // Assert + assert.deepStrictEqual(preferences, { + rememberChoice: false, + }) + }) + + it('should return stored preferences when available', function () { + // Setup + const storedPreferences: SmusAuthenticationPreferences = { + preferredMethod: 'iam', + rememberChoice: true, + lastUsedSsoConnection: 'conn-123', + lastUsedIamProfile: { + profileName: 'default', + region: 'us-east-1', + lastUsed: new Date('2023-01-01'), + isDefault: true, + }, + } + mockGlobalState.get.returns(storedPreferences) + + // Act + const preferences = SmusAuthenticationPreferencesManager.getPreferences(mockContext) + + // Assert + assert.deepStrictEqual(preferences, storedPreferences) + }) + + it('should merge stored preferences with defaults', function () { + // Setup + const partialPreferences = { + preferredMethod: 'sso' as const, + } + mockGlobalState.get.returns(partialPreferences) + + // Act + const preferences = SmusAuthenticationPreferencesManager.getPreferences(mockContext) + + // Assert + assert.deepStrictEqual(preferences, { + preferredMethod: 'sso', + rememberChoice: false, + }) + }) + }) + + describe('updatePreferences', function () { + it('should update preferences correctly', async function () { + // Setup + const currentPreferences: SmusAuthenticationPreferences = { + preferredMethod: 'sso', + rememberChoice: true, + } + mockGlobalState.get.returns(currentPreferences) + + const updates = { + preferredMethod: 'iam' as const, + lastUsedSsoConnection: 'conn-456', + } + + // Act + await SmusAuthenticationPreferencesManager.updatePreferences(mockContext, updates) + + // Assert + assert.strictEqual(mockGlobalState.update.calledOnce, true) + const [key, updatedPreferences] = mockGlobalState.update.firstCall.args + assert.strictEqual(key, 'aws.smus.authenticationPreferences') + assert.deepStrictEqual(updatedPreferences, { + preferredMethod: 'iam', + rememberChoice: true, + lastUsedSsoConnection: 'conn-456', + }) + }) + }) + + describe('setPreferredMethod', function () { + it('should set preferred method and remember choice', async function () { + // Setup + mockGlobalState.get.returns({}) + + // Act + await SmusAuthenticationPreferencesManager.setPreferredMethod(mockContext, 'iam', true) + + // Assert + assert.strictEqual(mockGlobalState.update.calledOnce, true) + const [key, preferences] = mockGlobalState.update.firstCall.args + assert.strictEqual(key, 'aws.smus.authenticationPreferences') + assert.deepStrictEqual(preferences, { + preferredMethod: 'iam', + rememberChoice: true, + }) + }) + }) + + describe('getPreferredMethod', function () { + it('should return preferred method when remember choice is true', function () { + // Setup + const preferences: SmusAuthenticationPreferences = { + preferredMethod: 'iam', + rememberChoice: true, + } + mockGlobalState.get.returns(preferences) + + // Act + const method = SmusAuthenticationPreferencesManager.getPreferredMethod(mockContext) + + // Assert + assert.strictEqual(method, 'iam') + }) + + it('should return undefined when remember choice is false', function () { + // Setup + const preferences: SmusAuthenticationPreferences = { + preferredMethod: 'iam', + rememberChoice: false, + } + mockGlobalState.get.returns(preferences) + + // Act + const method = SmusAuthenticationPreferencesManager.getPreferredMethod(mockContext) + + // Assert + assert.strictEqual(method, undefined) + }) + + it('should return undefined when no preferred method is set', function () { + // Setup + const preferences: SmusAuthenticationPreferences = { + rememberChoice: true, + } + mockGlobalState.get.returns(preferences) + + // Act + const method = SmusAuthenticationPreferencesManager.getPreferredMethod(mockContext) + + // Assert + assert.strictEqual(method, undefined) + }) + }) + + describe('setLastUsedSsoConnection', function () { + it('should set last used SSO connection', async function () { + // Setup + mockGlobalState.get.returns({}) + + // Act + await SmusAuthenticationPreferencesManager.setLastUsedSsoConnection(mockContext, 'conn-789') + + // Assert + assert.strictEqual(mockGlobalState.update.calledOnce, true) + const [key, preferences] = mockGlobalState.update.firstCall.args + assert.strictEqual(key, 'aws.smus.authenticationPreferences') + assert.deepStrictEqual(preferences, { + rememberChoice: false, + lastUsedSsoConnection: 'conn-789', + }) + }) + }) + + describe('setLastUsedIamProfile', function () { + it('should set last used IAM profile with timestamp', async function () { + // Setup + mockGlobalState.get.returns({}) + const profileConfig: SmusIamProfileConfig = { + profileName: 'production', + region: 'us-west-2', + isDefault: false, + } + + // Act + await SmusAuthenticationPreferencesManager.setLastUsedIamProfile(mockContext, profileConfig) + + // Assert + assert.strictEqual(mockGlobalState.update.calledOnce, true) + const [key, preferences] = mockGlobalState.update.firstCall.args + assert.strictEqual(key, 'aws.smus.authenticationPreferences') + + assert.strictEqual(preferences.lastUsedIamProfile.profileName, 'production') + assert.strictEqual(preferences.lastUsedIamProfile.region, 'us-west-2') + assert.strictEqual(preferences.lastUsedIamProfile.isDefault, false) + assert.ok(preferences.lastUsedIamProfile.lastUsed instanceof Date) + }) + }) + + describe('getLastUsedIamProfile', function () { + it('should return last used IAM profile when available', function () { + // Setup + const profileConfig: SmusIamProfileConfig = { + profileName: 'test-profile', + region: 'eu-west-1', + lastUsed: new Date('2023-06-01'), + isDefault: true, + } + const preferences: SmusAuthenticationPreferences = { + rememberChoice: false, + lastUsedIamProfile: profileConfig, + } + mockGlobalState.get.returns(preferences) + + // Act + const result = SmusAuthenticationPreferencesManager.getLastUsedIamProfile(mockContext) + + // Assert + assert.deepStrictEqual(result, profileConfig) + }) + + it('should return undefined when no IAM profile is stored', function () { + // Setup + mockGlobalState.get.returns({}) + + // Act + const result = SmusAuthenticationPreferencesManager.getLastUsedIamProfile(mockContext) + + // Assert + assert.strictEqual(result, undefined) + }) + }) + + describe('clearPreferences', function () { + it('should clear all preferences', async function () { + // Act + await SmusAuthenticationPreferencesManager.clearPreferences(mockContext) + + // Assert + assert.strictEqual(mockGlobalState.update.calledOnce, true) + const [key, value] = mockGlobalState.update.firstCall.args + assert.strictEqual(key, 'aws.smus.authenticationPreferences') + assert.strictEqual(value, undefined) + }) + }) + + describe('switchAuthenticationMethod', function () { + it('should switch authentication method', async function () { + // Setup + const currentPreferences: SmusAuthenticationPreferences = { + preferredMethod: 'sso', + rememberChoice: true, + } + mockGlobalState.get.returns(currentPreferences) + + // Act + await SmusAuthenticationPreferencesManager.switchAuthenticationMethod(mockContext, 'iam') + + // Assert + assert.strictEqual(mockGlobalState.update.calledOnce, true) + const [key, preferences] = mockGlobalState.update.firstCall.args + assert.strictEqual(key, 'aws.smus.authenticationPreferences') + assert.deepStrictEqual(preferences, { + preferredMethod: 'iam', + rememberChoice: true, + }) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/auth/projectRoleCredentialsProvider.test.ts b/packages/core/src/test/sagemakerunifiedstudio/auth/projectRoleCredentialsProvider.test.ts new file mode 100644 index 00000000000..bef7cc1885d --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/projectRoleCredentialsProvider.test.ts @@ -0,0 +1,259 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import { ProjectRoleCredentialsProvider } from '../../../sagemakerunifiedstudio/auth/providers/projectRoleCredentialsProvider' +import { DataZoneClient } from '../../../sagemakerunifiedstudio/shared/client/datazoneClient' +import { ToolkitError } from '../../../shared/errors' + +describe('ProjectRoleCredentialsProvider', function () { + let mockDataZoneClient: sinon.SinonStubbedInstance + let mockSmusAuthProvider: any + let projectProvider: ProjectRoleCredentialsProvider + let dataZoneClientStub: sinon.SinonStub + + const testProjectId = 'test-project-123' + const testDomainId = 'dzd_testdomain' + const testRegion = 'us-east-2' + + const mockGetEnvironmentCredentialsResponse = { + accessKeyId: 'AKIA-PROJECT-KEY', + secretAccessKey: 'project-secret-key', + sessionToken: 'project-session-token', + expiration: new Date(Date.now() + 14 * 60 * 1000), // 14 minutes as Date object + $metadata: { + httpStatusCode: 200, + requestId: 'test-request-id', + }, + } + + beforeEach(function () { + // Mock SMUS auth provider + mockSmusAuthProvider = { + getDomainId: sinon.stub().returns(testDomainId), + getDomainRegion: sinon.stub().returns(testRegion), + isConnected: sinon.stub().returns(true), + activeConnection: { + profileName: 'test-profile', + domainId: testDomainId, + ssoRegion: testRegion, + }, + getDerCredentialsProvider: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + getCredentialsProviderForIamProfile: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'profile-key', + secretAccessKey: 'profile-secret', + sessionToken: 'profile-token', + }), + }), + } as any + + // Mock DataZone client + mockDataZoneClient = { + getProjectDefaultEnvironmentCreds: sinon.stub().resolves(mockGetEnvironmentCredentialsResponse), + } as any + + dataZoneClientStub = sinon.stub(DataZoneClient, 'createWithCredentials').returns(mockDataZoneClient as any) + + projectProvider = new ProjectRoleCredentialsProvider(mockSmusAuthProvider, testProjectId) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('constructor', function () { + it('should initialize with DER provider and project ID', function () { + assert.strictEqual(projectProvider.getProjectId(), testProjectId) + }) + }) + + describe('getCredentialsId', function () { + it('should return correct credentials ID', function () { + const credentialsId = projectProvider.getCredentialsId() + assert.strictEqual(credentialsId.credentialSource, 'temp') + assert.strictEqual(credentialsId.credentialTypeId, `${testDomainId}:${testProjectId}`) + }) + }) + + describe('getProviderType', function () { + it('should return sso provider type', function () { + assert.strictEqual(projectProvider.getProviderType(), 'temp') + }) + }) + + describe('getTelemetryType', function () { + it('should return smusProfile telemetry type', function () { + assert.strictEqual(projectProvider.getTelemetryType(), 'other') + }) + }) + + describe('getDefaultRegion', function () { + it('should return DER provider default region', function () { + assert.strictEqual(projectProvider.getDefaultRegion(), testRegion) + }) + }) + + describe('getHashCode', function () { + it('should return correct hash code', function () { + const hashCode = projectProvider.getHashCode() + assert.strictEqual(hashCode, `smus-project:${testDomainId}:${testProjectId}`) + }) + }) + + describe('canAutoConnect', function () { + it('should return false', async function () { + const result = await projectProvider.canAutoConnect() + assert.strictEqual(result, false) + }) + }) + + describe('isAvailable', function () { + it('should delegate to SMUS auth provider', async function () { + const result = await projectProvider.isAvailable() + assert.strictEqual(result, true) + assert.ok(mockSmusAuthProvider.isConnected.called) + }) + }) + + describe('getCredentials', function () { + it('should fetch and cache project credentials', async function () { + const credentials = await projectProvider.getCredentials() + + // Verify DataZone client createWithCredentials was called with correct parameters + assert.ok(dataZoneClientStub.calledWith(testRegion, testDomainId, sinon.match.any)) + + // Verify getProjectDefaultEnvironmentCreds was called + assert.ok(mockDataZoneClient.getProjectDefaultEnvironmentCreds.called) + assert.ok(mockDataZoneClient.getProjectDefaultEnvironmentCreds.calledWith(testProjectId)) + + // Verify returned credentials + assert.strictEqual(credentials.accessKeyId, mockGetEnvironmentCredentialsResponse.accessKeyId) + assert.strictEqual(credentials.secretAccessKey, mockGetEnvironmentCredentialsResponse.secretAccessKey) + assert.strictEqual(credentials.sessionToken, mockGetEnvironmentCredentialsResponse.sessionToken) + assert.ok(credentials.expiration) + }) + + it('should use cached credentials when available', async function () { + // First call should fetch credentials + const credentials1 = await projectProvider.getCredentials() + + // Second call should use cache + const credentials2 = await projectProvider.getCredentials() + + // DataZone client method should only be called once + assert.strictEqual(mockDataZoneClient.getProjectDefaultEnvironmentCreds.callCount, 1) + + // Credentials should be the same + assert.strictEqual(credentials1, credentials2) + }) + + it('should handle DataZone client errors', async function () { + const error = new Error('DataZone client failed') + mockDataZoneClient.getProjectDefaultEnvironmentCreds.rejects(error) + + await assert.rejects( + () => projectProvider.getCredentials(), + (err: ToolkitError) => { + return err.code === 'ProjectCredentialsFetchFailed' && err.message.includes(testProjectId) + } + ) + }) + + it('should handle GetEnvironmentCredentials API errors', async function () { + const error = new Error('API call failed') + mockDataZoneClient.getProjectDefaultEnvironmentCreds.rejects(error) + + await assert.rejects( + () => projectProvider.getCredentials(), + (err: ToolkitError) => { + return err.code === 'ProjectCredentialsFetchFailed' + } + ) + }) + + it('should handle missing credentials in response', async function () { + mockDataZoneClient.getProjectDefaultEnvironmentCreds.resolves({ + accessKeyId: undefined, + $metadata: { + httpStatusCode: 200, + requestId: 'test-request-id', + }, + }) + + await assert.rejects( + () => projectProvider.getCredentials(), + (err: ToolkitError) => { + return err.code === 'ProjectCredentialsFetchFailed' + } + ) + }) + + it('should handle invalid credential fields', async function () { + const invalidResponse = { + accessKeyId: '', // Invalid empty string + secretAccessKey: 'valid-secret', + sessionToken: 'valid-token', + $metadata: { + httpStatusCode: 200, + requestId: 'test-request-id', + }, + } + mockDataZoneClient.getProjectDefaultEnvironmentCreds.resolves(invalidResponse) + + await assert.rejects( + () => projectProvider.getCredentials(), + (err: ToolkitError) => { + return err.code === 'ProjectCredentialsFetchFailed' + } + ) + }) + + it('should use default expiration when not provided in response', async function () { + const responseWithoutExpiration = { + accessKeyId: 'AKIA-PROJECT-KEY', + secretAccessKey: 'project-secret-key', + sessionToken: 'project-session-token', + // No expiration field + $metadata: { + httpStatusCode: 200, + requestId: 'test-request-id', + }, + } + mockDataZoneClient.getProjectDefaultEnvironmentCreds.resolves(responseWithoutExpiration) + + const credentials = await projectProvider.getCredentials() + + // Should have expiration set to ~10 minutes from now + assert.ok(credentials.expiration) + const expirationTime = credentials.expiration!.getTime() + const expectedTime = Date.now() + 10 * 60 * 1000 + const timeDiff = Math.abs(expirationTime - expectedTime) + assert.ok(timeDiff < 5000, 'Expiration should be ~10 minutes from now') + }) + }) + + describe('invalidate', function () { + it('should clear cache and force fresh fetch on next call', async function () { + // First call to populate cache + await projectProvider.getCredentials() + assert.strictEqual(mockDataZoneClient.getProjectDefaultEnvironmentCreds.callCount, 1) + + // Invalidate should clear cache + projectProvider.invalidate() + + // Next call should fetch fresh credentials + await projectProvider.getCredentials() + assert.strictEqual(mockDataZoneClient.getProjectDefaultEnvironmentCreds.callCount, 2) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/auth/smusAuthenticationProvider.test.ts b/packages/core/src/test/sagemakerunifiedstudio/auth/smusAuthenticationProvider.test.ts new file mode 100644 index 00000000000..60f8558c9ba --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/smusAuthenticationProvider.test.ts @@ -0,0 +1,1846 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import * as vscode from 'vscode' +import * as setContextModule from '../../../shared/vscode/setContext' +import * as secondaryAuthModule from '../../../auth/secondaryAuth' +import * as sharedCredentialsModule from '../../../auth/credentials/sharedCredentials' +import * as stsClientModule from '../../../shared/clients/stsClient' +import { DataZoneCustomClientHelper } from '../../../sagemakerunifiedstudio/shared/client/datazoneCustomClientHelper' + +import { SmusAuthenticationProvider } from '../../../sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider' +import { SmusConnection } from '../../../sagemakerunifiedstudio/auth/model' +import { DataZoneClient } from '../../../sagemakerunifiedstudio/shared/client/datazoneClient' +import { SmusUtils } from '../../../sagemakerunifiedstudio/shared/smusUtils' +import * as smusUtils from '../../../sagemakerunifiedstudio/shared/smusUtils' +import { ToolkitError } from '../../../shared/errors' +import * as messages from '../../../shared/utilities/messages' +import * as vscodeSetContext from '../../../shared/vscode/setContext' +import * as resourceMetadataUtils from '../../../sagemakerunifiedstudio/shared/utils/resourceMetadataUtils' +import { DefaultStsClient } from '../../../shared/clients/stsClient' + +describe('SmusAuthenticationProvider', function () { + let mockAuth: any + let mockSecondaryAuth: any + let mockDataZoneClient: sinon.SinonStubbedInstance + let smusAuthProvider: SmusAuthenticationProvider + let extractDomainInfoStub: sinon.SinonStub + let getSsoInstanceInfoStub: sinon.SinonStub + let isInSmusSpaceEnvironmentStub: sinon.SinonStub + let executeCommandStub: sinon.SinonStub + let setContextStubGlobal: sinon.SinonStub + let getResourceMetadataStub: sinon.SinonStub + let mockSecondaryAuthState: { + activeConnection: SmusConnection | undefined + hasSavedConnection: boolean + isConnectionExpired: boolean + } + + const testDomainUrl = 'https://dzd_domainId.sagemaker.us-east-2.on.aws' + const testDomainId = 'dzd_domainId' + const testRegion = 'us-east-2' + const testSsoInstanceInfo = { + issuerUrl: 'https://identitycenter.amazonaws.com/ssoins-testInstanceId', + ssoInstanceId: 'ssoins-testInstanceId', + clientId: 'arn:aws:sso::123456789:application/ssoins-testInstanceId/apl-testAppId', + region: testRegion, + } + + const mockSmusConnection: SmusConnection = { + id: 'test-connection-id', + type: 'sso', + startUrl: 'https://identitycenter.amazonaws.com/ssoins-testInstanceId', + ssoRegion: testRegion, + scopes: ['datazone:domain:access'], + label: 'Test SMUS Connection', + domainUrl: testDomainUrl, + domainId: testDomainId, + getToken: sinon.stub().resolves({ accessToken: 'mock-token', expiresAt: new Date() }), + getRegistration: sinon.stub().resolves({ clientId: 'mock-client', expiresAt: new Date() }), + } + + beforeEach(function () { + // Create the setContext stub + setContextStubGlobal = sinon.stub(setContextModule, 'setContext').resolves() + + mockAuth = { + createConnection: sinon.stub().resolves(mockSmusConnection), + listConnections: sinon.stub().resolves([]), + getConnectionState: sinon.stub().returns('valid'), + reauthenticate: sinon.stub().resolves(mockSmusConnection), + } as any + + // Create a mock object with configurable properties + mockSecondaryAuthState = { + activeConnection: mockSmusConnection as SmusConnection | undefined, + hasSavedConnection: false, + isConnectionExpired: false, + } + + mockSecondaryAuth = { + get activeConnection() { + return mockSecondaryAuthState.activeConnection + }, + get hasSavedConnection() { + return mockSecondaryAuthState.hasSavedConnection + }, + get isConnectionExpired() { + return mockSecondaryAuthState.isConnectionExpired + }, + state: { + get: sinon.stub().returns({}), + update: sinon.stub().resolves(), + }, + onDidChangeActiveConnection: sinon.stub().returns({ dispose: sinon.stub() }), + restoreConnection: sinon.stub().resolves(), + useNewConnection: sinon.stub().resolves(mockSmusConnection), + deleteConnection: sinon.stub().resolves(), + } + + mockDataZoneClient = { + // Add any DataZoneClient methods that might be used + } as any + + // Stub static methods + sinon.stub(DataZoneClient, 'createWithCredentials').returns(mockDataZoneClient as any) + extractDomainInfoStub = sinon + .stub(SmusUtils, 'extractDomainInfoFromUrl') + .returns({ domainId: testDomainId, region: testRegion }) + getSsoInstanceInfoStub = sinon.stub(SmusUtils, 'getSsoInstanceInfo').resolves(testSsoInstanceInfo) + isInSmusSpaceEnvironmentStub = sinon.stub(SmusUtils, 'isInSmusSpaceEnvironment').returns(false) + executeCommandStub = sinon.stub(vscode.commands, 'executeCommand').resolves() + sinon.stub(secondaryAuthModule, 'getSecondaryAuth').returns(mockSecondaryAuth) + + smusAuthProvider = new SmusAuthenticationProvider(mockAuth) + + // Reset the executeCommand stub for clean state + executeCommandStub.resetHistory() + }) + + afterEach(function () { + sinon.restore() + }) + + describe('constructor', function () { + it('should initialize with auth and secondary auth', function () { + assert.strictEqual(smusAuthProvider.auth, mockAuth) + assert.strictEqual(smusAuthProvider.secondaryAuth, mockSecondaryAuth) + }) + + it('should register event listeners', function () { + assert.ok(mockSecondaryAuth.onDidChangeActiveConnection.called) + }) + + it('should set initial context', async function () { + // Context should be set during construction (async call) + // Wait a bit for the async call to complete + await new Promise((resolve) => setTimeout(resolve, 0)) + assert.ok(setContextStubGlobal.called) + }) + }) + + describe('activeConnection', function () { + it('should return secondary auth active connection', function () { + assert.strictEqual(smusAuthProvider.activeConnection, mockSmusConnection) + }) + }) + + describe('isUsingSavedConnection', function () { + it('should return secondary auth hasSavedConnection value', function () { + mockSecondaryAuthState.hasSavedConnection = true + assert.strictEqual(smusAuthProvider.isUsingSavedConnection, true) + + mockSecondaryAuthState.hasSavedConnection = false + assert.strictEqual(smusAuthProvider.isUsingSavedConnection, false) + }) + }) + + describe('isConnectionValid', function () { + it('should return true when connection exists and is not expired', function () { + mockSecondaryAuthState.activeConnection = mockSmusConnection + mockSecondaryAuthState.isConnectionExpired = false + + assert.strictEqual(smusAuthProvider.isConnectionValid(), true) + }) + + it('should return false when no connection exists', function () { + mockSecondaryAuthState.activeConnection = undefined + + assert.strictEqual(smusAuthProvider.isConnectionValid(), false) + }) + + it('should return false when connection is expired', function () { + mockSecondaryAuthState.activeConnection = mockSmusConnection + mockSecondaryAuthState.isConnectionExpired = true + + assert.strictEqual(smusAuthProvider.isConnectionValid(), false) + }) + }) + + describe('isConnected', function () { + it('should return true when active connection exists', function () { + mockSecondaryAuthState.activeConnection = mockSmusConnection + assert.strictEqual(smusAuthProvider.isConnected(), true) + }) + + it('should return false when no active connection', function () { + mockSecondaryAuthState.activeConnection = undefined + assert.strictEqual(smusAuthProvider.isConnected(), false) + }) + }) + + describe('restore', function () { + let mockState: any + let loadSharedCredentialsProfilesStub: sinon.SinonStub + let validateIamProfileStub: sinon.SinonStub + beforeEach(function () { + mockState = { + get: sinon.stub(), + update: sinon.stub().resolves(), + } + mockSecondaryAuth.state = mockState + + loadSharedCredentialsProfilesStub = sinon.stub(sharedCredentialsModule, 'loadSharedCredentialsProfiles') + validateIamProfileStub = sinon.stub(smusAuthProvider, 'validateIamProfile') + }) + + it('should call secondary auth restoreConnection when no saved connection ID', async function () { + mockState.get.withArgs('smus.savedConnectionId').returns(undefined) + + await smusAuthProvider.restore() + + assert.ok(mockSecondaryAuth.restoreConnection.called) + assert.ok(loadSharedCredentialsProfilesStub.notCalled) + }) + + it('should validate IAM profile and restore connection', async function () { + const savedConnectionId = 'test-connection-id' + const connectionMetadata = { + profileName: 'test-profile', + domainId: 'old-domain-id', + region: 'us-west-1', + } + const smusConnections = { [savedConnectionId]: connectionMetadata } + + mockState.get.withArgs('smus.savedConnectionId').returns(savedConnectionId) + mockState.get.withArgs('smus.connections').returns(smusConnections) + loadSharedCredentialsProfilesStub.resolves({ 'test-profile': { region: 'us-east-1' } }) + validateIamProfileStub.resolves({ isValid: true }) + + await smusAuthProvider.restore() + + assert.ok(validateIamProfileStub.calledWith('test-profile')) + assert.ok(mockSecondaryAuth.restoreConnection.called) + }) + }) + + describe('connectToSmusWithSso', function () { + it('should create new connection when none exists', async function () { + mockAuth.listConnections.resolves([]) + + const result = await smusAuthProvider.connectToSmusWithSso(testDomainUrl) + + assert.strictEqual(result, mockSmusConnection) + assert.ok(extractDomainInfoStub.calledWith(testDomainUrl)) + assert.ok(getSsoInstanceInfoStub.calledWith(testDomainUrl)) + assert.ok(mockAuth.createConnection.called) + assert.ok(mockSecondaryAuth.useNewConnection.called) + assert.ok(executeCommandStub.calledWith('aws.smus.switchProject')) + }) + + it('should reuse existing valid connection', async function () { + const existingConnection = { ...mockSmusConnection, domainUrl: testDomainUrl.toLowerCase() } + mockAuth.listConnections.resolves([existingConnection]) + mockAuth.getConnectionState.returns('valid') + + const result = await smusAuthProvider.connectToSmusWithSso(testDomainUrl) + + assert.strictEqual(result, mockSmusConnection) + assert.ok(mockAuth.createConnection.notCalled) + assert.ok(mockSecondaryAuth.useNewConnection.calledWith(existingConnection)) + assert.ok(executeCommandStub.calledWith('aws.smus.switchProject')) + }) + + it('should reauthenticate existing invalid connection', async function () { + const existingConnection = { ...mockSmusConnection, domainUrl: testDomainUrl.toLowerCase() } + mockAuth.listConnections.resolves([existingConnection]) + mockAuth.getConnectionState.returns('invalid') + + const result = await smusAuthProvider.connectToSmusWithSso(testDomainUrl) + + assert.strictEqual(result, mockSmusConnection) + assert.ok(mockAuth.reauthenticate.calledWith(existingConnection)) + assert.ok(mockSecondaryAuth.useNewConnection.called) + assert.ok(executeCommandStub.calledWith('aws.smus.switchProject')) + }) + + it('should throw error for invalid domain URL', async function () { + extractDomainInfoStub.returns({ domainId: undefined, region: testRegion }) + + await assert.rejects( + () => smusAuthProvider.connectToSmusWithSso('invalid-url'), + (err: ToolkitError) => { + // The error is wrapped with FailedToConnect, but the original error should be in the cause + return err.code === 'FailedToConnect' && (err.cause as any)?.code === 'InvalidDomainUrl' + } + ) + // Should not trigger project selection on error + assert.ok(executeCommandStub.notCalled) + }) + + it('should handle SmusUtils errors', async function () { + const error = new Error('SmusUtils error') + getSsoInstanceInfoStub.rejects(error) + + await assert.rejects( + () => smusAuthProvider.connectToSmusWithSso(testDomainUrl), + (err: ToolkitError) => err.code === 'FailedToConnect' + ) + // Should not trigger project selection on error + assert.ok(executeCommandStub.notCalled) + }) + + it('should handle auth creation errors', async function () { + const error = new Error('Auth creation failed') + mockAuth.createConnection.rejects(error) + + await assert.rejects( + () => smusAuthProvider.connectToSmusWithSso(testDomainUrl), + (err: ToolkitError) => err.code === 'FailedToConnect' + ) + // Should not trigger project selection on error + assert.ok(executeCommandStub.notCalled) + }) + + it('should not trigger project selection in SMUS space environment', async function () { + isInSmusSpaceEnvironmentStub.returns(true) + mockAuth.listConnections.resolves([]) + + const result = await smusAuthProvider.connectToSmusWithSso(testDomainUrl) + + assert.strictEqual(result, mockSmusConnection) + assert.ok(mockAuth.createConnection.called) + assert.ok(mockSecondaryAuth.useNewConnection.called) + assert.ok(executeCommandStub.notCalled) + }) + + it('should not trigger project selection when reusing connection in SMUS space environment', async function () { + isInSmusSpaceEnvironmentStub.returns(true) + const existingConnection = { ...mockSmusConnection, domainUrl: testDomainUrl.toLowerCase() } + mockAuth.listConnections.resolves([existingConnection]) + mockAuth.getConnectionState.returns('valid') + + const result = await smusAuthProvider.connectToSmusWithSso(testDomainUrl) + + assert.strictEqual(result, mockSmusConnection) + assert.ok(mockSecondaryAuth.useNewConnection.calledWith(existingConnection)) + assert.ok(executeCommandStub.notCalled) + }) + + it('should not trigger project selection when reauthenticating in SMUS space environment', async function () { + isInSmusSpaceEnvironmentStub.returns(true) + const existingConnection = { ...mockSmusConnection, domainUrl: testDomainUrl.toLowerCase() } + mockAuth.listConnections.resolves([existingConnection]) + mockAuth.getConnectionState.returns('invalid') + + const result = await smusAuthProvider.connectToSmusWithSso(testDomainUrl) + + assert.strictEqual(result, mockSmusConnection) + assert.ok(mockAuth.reauthenticate.calledWith(existingConnection)) + assert.ok(mockSecondaryAuth.useNewConnection.called) + assert.ok(executeCommandStub.notCalled) + }) + }) + + describe('reauthenticate', function () { + it('should call auth reauthenticate for SSO connection', async function () { + const result = await smusAuthProvider.reauthenticate(mockSmusConnection) + + // Verify the result has the correct SMUS properties preserved + assert.strictEqual(result.id, mockSmusConnection.id) + assert.strictEqual(result.domainUrl, mockSmusConnection.domainUrl) + assert.strictEqual(result.domainId, mockSmusConnection.domainId) + assert.strictEqual(result.type, mockSmusConnection.type) + assert.strictEqual(result.startUrl, mockSmusConnection.startUrl) + assert.strictEqual(result.label, mockSmusConnection.label) + assert.ok(mockAuth.reauthenticate.calledWith(mockSmusConnection)) + }) + + it('should wrap auth errors in ToolkitError', async function () { + const error = new Error('Reauthentication failed') + mockAuth.reauthenticate.rejects(error) + + await assert.rejects( + () => smusAuthProvider.reauthenticate(mockSmusConnection), + (err: ToolkitError) => err.message.includes('Unable to reauthenticate') + ) + }) + }) + + describe('showReauthenticationPrompt', function () { + it('should show reauthentication message', async function () { + const showReauthenticateMessageStub = sinon.stub(messages, 'showReauthenticateMessage').resolves() + + await smusAuthProvider.showReauthenticationPrompt(mockSmusConnection) + + assert.ok(showReauthenticateMessageStub.called) + const callArgs = showReauthenticateMessageStub.firstCall.args[0] + assert.ok(callArgs.message.includes('SageMaker Unified Studio')) + assert.strictEqual(callArgs.suppressId, 'smusConnectionExpired') + }) + }) + + describe('getAccessToken', function () { + beforeEach(function () { + mockSecondaryAuthState.activeConnection = mockSmusConnection + mockAuth.getSsoAccessToken = sinon.stub().resolves('mock-access-token') + mockAuth.invalidateConnection = sinon.stub() + }) + + it('should return access token when successful', async function () { + const token = await smusAuthProvider.getAccessToken() + + assert.strictEqual(token, 'mock-access-token') + assert.ok(mockAuth.getSsoAccessToken.calledWith(mockSmusConnection)) + }) + + it('should throw error when no active connection', async function () { + mockSecondaryAuthState.activeConnection = undefined + + await assert.rejects( + () => smusAuthProvider.getAccessToken(), + (err: ToolkitError) => err.code === 'NoActiveConnection' + ) + }) + + it('should handle InvalidGrantException and mark connection for reauthentication', async function () { + const invalidGrantError = new Error('UnknownError') + invalidGrantError.name = 'InvalidGrantException' + mockAuth.getSsoAccessToken.rejects(invalidGrantError) + + await assert.rejects( + () => smusAuthProvider.getAccessToken(), + (err: ToolkitError) => { + return ( + err.code === 'RedeemAccessTokenFailed' && + err.message.includes('Failed to retrieve SSO access token for connection') + ) + } + ) + + // Verify connection was NOT invalidated (current implementation doesn't handle InvalidGrantException specially) + assert.ok(mockAuth.invalidateConnection.notCalled) + }) + + it('should handle other errors normally', async function () { + const genericError = new Error('Network error') + mockAuth.getSsoAccessToken.rejects(genericError) + + await assert.rejects( + () => smusAuthProvider.getAccessToken(), + (err: ToolkitError) => + err.message.includes('Failed to retrieve SSO access token for connection') && + err.code === 'RedeemAccessTokenFailed' + ) + + // Verify connection was NOT invalidated for generic errors + assert.ok(mockAuth.invalidateConnection.notCalled) + }) + }) + + describe('fromContext', function () { + it('should return singleton instance', function () { + const instance1 = SmusAuthenticationProvider.fromContext() + const instance2 = SmusAuthenticationProvider.fromContext() + + assert.strictEqual(instance1, instance2) + }) + + it('should return instance property', function () { + const instance = SmusAuthenticationProvider.fromContext() + assert.strictEqual(SmusAuthenticationProvider.instance, instance) + }) + }) + + describe('getDomainAccountId', function () { + let getContextStub: sinon.SinonStub + let getResourceMetadataStub: sinon.SinonStub + let getDerCredentialsProviderStub: sinon.SinonStub + let getDomainRegionStub: sinon.SinonStub + let mockStsClient: any + let mockCredentialsProvider: any + + beforeEach(function () { + // Mock dependencies + getContextStub = sinon.stub(vscodeSetContext, 'getContext') + getResourceMetadataStub = sinon.stub(resourceMetadataUtils, 'getResourceMetadata') + + // Mock STS client + mockStsClient = { + getCallerIdentity: sinon.stub(), + } + sinon + .stub(DefaultStsClient.prototype, 'getCallerIdentity') + .callsFake(() => mockStsClient.getCallerIdentity()) + + // Mock credentials provider + mockCredentialsProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + + // Stub methods on the provider instance + getDerCredentialsProviderStub = sinon + .stub(smusAuthProvider, 'getDerCredentialsProvider') + .resolves(mockCredentialsProvider) + getDomainRegionStub = sinon.stub(smusAuthProvider, 'getDomainRegion').returns('us-east-1') + + // Reset cached value + smusAuthProvider['cachedDomainAccountId'] = undefined + }) + + afterEach(function () { + sinon.restore() + }) + + describe('when cached value exists', function () { + it('should return cached account ID without making any calls', async function () { + const cachedAccountId = '123456789012' + smusAuthProvider['cachedDomainAccountId'] = cachedAccountId + + const result = await smusAuthProvider.getDomainAccountId() + + assert.strictEqual(result, cachedAccountId) + assert.ok(getContextStub.notCalled) + assert.ok(getResourceMetadataStub.notCalled) + assert.ok(mockStsClient.getCallerIdentity.notCalled) + }) + }) + + describe('in SMUS space environment', function () { + let extractAccountIdFromResourceMetadataStub: sinon.SinonStub + + beforeEach(function () { + getContextStub.withArgs('aws.smus.inSmusSpaceEnvironment').returns(true) + extractAccountIdFromResourceMetadataStub = sinon + .stub(smusUtils, 'extractAccountIdFromResourceMetadata') + .resolves('123456789012') + }) + + it('should extract account from resource metadata and cache result', async function () { + const testAccountId = '123456789012' + + const result = await smusAuthProvider.getDomainAccountId() + + assert.strictEqual(result, testAccountId) + assert.strictEqual(smusAuthProvider['cachedDomainAccountId'], testAccountId) + assert.ok(extractAccountIdFromResourceMetadataStub.called) + assert.ok(mockStsClient.getCallerIdentity.notCalled) + }) + + it('should throw error when extractAccountIdFromResourceMetadata fails', async function () { + extractAccountIdFromResourceMetadataStub.rejects(new ToolkitError('Metadata extraction failed')) + + await assert.rejects( + () => smusAuthProvider.getDomainAccountId(), + (err: ToolkitError) => err.message.includes('Metadata extraction failed') + ) + + assert.strictEqual(smusAuthProvider['cachedDomainAccountId'], undefined) + }) + }) + + describe('in non-SMUS space environment', function () { + beforeEach(function () { + getContextStub.withArgs('aws.smus.inSmusSpaceEnvironment').returns(false) + mockSecondaryAuthState.activeConnection = mockSmusConnection + }) + + it('should use STS GetCallerIdentity to get account ID and cache it', async function () { + const testAccountId = '123456789012' + mockStsClient.getCallerIdentity.resolves({ + Account: testAccountId, + UserId: 'test-user-id', + Arn: 'arn:aws:sts::123456789012:assumed-role/test-role/test-session', + }) + + const result = await smusAuthProvider.getDomainAccountId() + + assert.strictEqual(result, testAccountId) + assert.strictEqual(smusAuthProvider['cachedDomainAccountId'], testAccountId) + assert.ok(getDerCredentialsProviderStub.called) + assert.ok(getDomainRegionStub.called) + assert.ok(mockCredentialsProvider.getCredentials.called) + assert.ok(mockStsClient.getCallerIdentity.called) + }) + + it('should throw error when no active connection exists', async function () { + mockSecondaryAuthState.activeConnection = undefined + + await assert.rejects( + () => smusAuthProvider.getDomainAccountId(), + (err: ToolkitError) => { + return ( + err.code === 'NoActiveConnection' && + err.message.includes('No active SMUS connection available') + ) + } + ) + + assert.strictEqual(smusAuthProvider['cachedDomainAccountId'], undefined) + assert.ok(getDerCredentialsProviderStub.notCalled) + assert.ok(mockStsClient.getCallerIdentity.notCalled) + }) + + it('should throw error when STS GetCallerIdentity fails', async function () { + mockStsClient.getCallerIdentity.rejects(new Error('STS call failed')) + + await assert.rejects( + () => smusAuthProvider.getDomainAccountId(), + (err: ToolkitError) => { + return ( + err.code === 'GetDomainAccountIdFailed' && + err.message.includes('Failed to retrieve AWS account ID for active domain connection') + ) + } + ) + + assert.strictEqual(smusAuthProvider['cachedDomainAccountId'], undefined) + }) + }) + }) + + describe('getProjectAccountId', function () { + let getContextStub: sinon.SinonStub + let extractAccountIdFromResourceMetadataStub: sinon.SinonStub + let getProjectCredentialProviderStub: sinon.SinonStub + let mockProjectCredentialsProvider: any + let mockStsClient: any + let mockDataZoneClientForProject: any + + const testProjectId = 'test-project-id' + const testAccountId = '123456789012' + const testRegion = 'us-east-1' + + beforeEach(function () { + // Mock dependencies + getContextStub = sinon.stub(vscodeSetContext, 'getContext') + extractAccountIdFromResourceMetadataStub = sinon + .stub(smusUtils, 'extractAccountIdFromResourceMetadata') + .resolves(testAccountId) + + // Mock project credentials provider + mockProjectCredentialsProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + getProjectCredentialProviderStub = sinon + .stub(smusAuthProvider, 'getProjectCredentialProvider') + .resolves(mockProjectCredentialsProvider) + + // Update the existing mockDataZoneClient to include getToolingEnvironment + mockDataZoneClientForProject = { + getToolingEnvironment: sinon.stub().resolves({ + awsAccountRegion: testRegion, + projectId: testProjectId, + domainId: testDomainId, + createdBy: 'test-user', + name: 'test-environment', + id: 'test-env-id', + status: 'ACTIVE', + }), + } + // Update the existing mockDataZoneClient instead of creating a new stub + Object.assign(mockDataZoneClient, mockDataZoneClientForProject) + + // Mock STS client + mockStsClient = { + getCallerIdentity: sinon.stub().resolves({ + Account: testAccountId, + UserId: 'test-user-id', + Arn: 'arn:aws:sts::123456789012:assumed-role/test-role/test-session', + }), + } + + // Clear cache + smusAuthProvider['cachedProjectAccountIds'].clear() + mockSecondaryAuthState.activeConnection = mockSmusConnection + }) + + afterEach(function () { + sinon.restore() + }) + + describe('when cached value exists', function () { + it('should return cached project account ID without making any calls', async function () { + smusAuthProvider['cachedProjectAccountIds'].set(testProjectId, testAccountId) + + const result = await smusAuthProvider.getProjectAccountId(testProjectId) + + assert.strictEqual(result, testAccountId) + assert.ok(getContextStub.notCalled) + assert.ok(extractAccountIdFromResourceMetadataStub.notCalled) + assert.ok(getProjectCredentialProviderStub.notCalled) + assert.ok(mockStsClient.getCallerIdentity.notCalled) + }) + }) + + describe('in SMUS space environment', function () { + beforeEach(function () { + getContextStub.withArgs('aws.smus.inSmusSpaceEnvironment').returns(true) + }) + + it('should extract account ID from resource metadata and cache it', async function () { + const result = await smusAuthProvider.getProjectAccountId(testProjectId) + + assert.strictEqual(result, testAccountId) + assert.strictEqual(smusAuthProvider['cachedProjectAccountIds'].get(testProjectId), testAccountId) + assert.ok(extractAccountIdFromResourceMetadataStub.called) + assert.ok(getProjectCredentialProviderStub.notCalled) + assert.ok(mockStsClient.getCallerIdentity.notCalled) + }) + + it('should throw error when extractAccountIdFromResourceMetadata fails', async function () { + extractAccountIdFromResourceMetadataStub.rejects(new ToolkitError('Metadata extraction failed')) + + await assert.rejects( + () => smusAuthProvider.getProjectAccountId(testProjectId), + (err: ToolkitError) => err.message.includes('Metadata extraction failed') + ) + + assert.ok(!smusAuthProvider['cachedProjectAccountIds'].has(testProjectId)) + }) + }) + + describe('in non-SMUS space environment', function () { + let stsConstructorStub: sinon.SinonStub + + beforeEach(function () { + getContextStub.withArgs('aws.smus.inSmusSpaceEnvironment').returns(false) + // Stub the DefaultStsClient constructor to return our mock instance + // stsClientModule imported at top + stsConstructorStub = sinon.stub(stsClientModule, 'DefaultStsClient').callsFake(() => mockStsClient) + }) + + afterEach(function () { + if (stsConstructorStub) { + stsConstructorStub.restore() + } + }) + + it('should use project credentials with STS to get account ID and cache it', async function () { + const result = await smusAuthProvider.getProjectAccountId(testProjectId) + + assert.strictEqual(result, testAccountId) + assert.strictEqual(smusAuthProvider['cachedProjectAccountIds'].get(testProjectId), testAccountId) + assert.ok(getProjectCredentialProviderStub.calledWith(testProjectId)) + assert.ok(mockProjectCredentialsProvider.getCredentials.called) + assert.ok((DataZoneClient.createWithCredentials as sinon.SinonStub).called) + assert.ok(mockDataZoneClientForProject.getToolingEnvironment.calledWith(testProjectId)) + assert.ok(mockStsClient.getCallerIdentity.called) + }) + + it('should throw error when no active connection exists', async function () { + mockSecondaryAuthState.activeConnection = undefined + + await assert.rejects( + () => smusAuthProvider.getProjectAccountId(testProjectId), + (err: ToolkitError) => { + return ( + err.code === 'NoActiveConnection' && + err.message.includes('No active SMUS connection available') + ) + } + ) + + assert.ok(!smusAuthProvider['cachedProjectAccountIds'].has(testProjectId)) + }) + + it('should throw error when tooling environment has no region', async function () { + mockDataZoneClientForProject.getToolingEnvironment.resolves({ + id: 'env-123', + awsAccountRegion: undefined, + projectId: undefined, + domainId: undefined, + createdBy: undefined, + name: undefined, + provider: undefined, + $metadata: {}, + }) + + await assert.rejects( + () => smusAuthProvider.getProjectAccountId(testProjectId), + (err: ToolkitError) => { + return ( + err.message.includes('Failed to get project account ID') && + err.message.includes('No AWS account region found in tooling environment') + ) + } + ) + + assert.ok(!smusAuthProvider['cachedProjectAccountIds'].has(testProjectId)) + }) + + it('should throw error when STS GetCallerIdentity fails', async function () { + mockStsClient.getCallerIdentity.rejects(new Error('STS call failed')) + + await assert.rejects( + () => smusAuthProvider.getProjectAccountId(testProjectId), + (err: ToolkitError) => { + return ( + err.message.includes('Failed to get project account ID') && + err.message.includes('STS call failed') + ) + } + ) + + assert.ok(!smusAuthProvider['cachedProjectAccountIds'].has(testProjectId)) + }) + }) + }) + + describe('signOut', function () { + let mockState: any + + beforeEach(function () { + mockState = { + get: sinon.stub(), + update: sinon.stub().resolves(), + } + mockSecondaryAuth.state = mockState + mockSecondaryAuth.forgetConnection = sinon.stub().resolves() + }) + + it('should do nothing when no active connection exists', async function () { + mockSecondaryAuthState.activeConnection = undefined + + await smusAuthProvider.signOut() + + assert.ok(mockState.get.notCalled) + assert.ok(mockState.update.notCalled) + assert.ok(mockSecondaryAuth.deleteConnection.notCalled) + assert.ok(mockSecondaryAuth.forgetConnection.notCalled) + }) + + it('should delete SSO connection and clear metadata', async function () { + const ssoConnection = { + ...mockSmusConnection, + type: 'sso' as const, + id: 'sso-connection-id', + } + mockSecondaryAuthState.activeConnection = ssoConnection + + const smusConnections = { + 'sso-connection-id': { + domainUrl: testDomainUrl, + domainId: testDomainId, + }, + } + mockState.get.withArgs('smus.connections').returns(smusConnections) + + await smusAuthProvider.signOut() + + assert.ok(mockState.get.calledWith('smus.connections')) + assert.ok(mockState.update.calledWith('smus.connections', {})) + assert.ok(mockSecondaryAuth.deleteConnection.called) + assert.ok(mockSecondaryAuth.forgetConnection.notCalled) + }) + + it('should forget IAM connection without deleting and clear metadata', async function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + const smusConnections = { + 'profile:test-profile': { + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + }, + } + mockState.get.withArgs('smus.connections').returns(smusConnections) + + await smusAuthProvider.signOut() + + assert.ok(mockState.get.calledWith('smus.connections')) + assert.ok(mockState.update.calledWith('smus.connections', {})) + assert.ok(mockSecondaryAuth.forgetConnection.called) + assert.ok(mockSecondaryAuth.deleteConnection.notCalled) + }) + + it('should handle mock connection in SMUS space environment', async function () { + const mockConnection = { + id: 'mock-connection-id', + // No 'type' property - simulates mock connection + } + mockSecondaryAuthState.activeConnection = mockConnection as any + + const smusConnections = { + 'mock-connection-id': { + domainUrl: testDomainUrl, + domainId: testDomainId, + }, + } + mockState.get.withArgs('smus.connections').returns(smusConnections) + + await smusAuthProvider.signOut() + + assert.ok(mockState.get.calledWith('smus.connections')) + assert.ok(mockState.update.calledWith('smus.connections', {})) + assert.ok(mockSecondaryAuth.deleteConnection.notCalled) + assert.ok(mockSecondaryAuth.forgetConnection.notCalled) + }) + + it('should handle missing metadata gracefully', async function () { + const ssoConnection = { + ...mockSmusConnection, + type: 'sso' as const, + id: 'sso-connection-id', + } + mockSecondaryAuthState.activeConnection = ssoConnection + + mockState.get.withArgs('smus.connections').returns({}) + + await smusAuthProvider.signOut() + + assert.ok(mockState.get.calledWith('smus.connections')) + // When there's no metadata to delete, update should not be called + assert.ok(mockState.update.notCalled) + assert.ok(mockSecondaryAuth.deleteConnection.called) + }) + + it('should throw ToolkitError when deleteConnection fails', async function () { + const ssoConnection = { + ...mockSmusConnection, + type: 'sso' as const, + id: 'sso-connection-id', + } + mockSecondaryAuthState.activeConnection = ssoConnection + + mockState.get.withArgs('smus.connections').returns({}) + mockSecondaryAuth.deleteConnection.rejects(new Error('Delete failed')) + + await assert.rejects( + () => smusAuthProvider.signOut(), + (err: ToolkitError) => { + return ( + err.code === 'SignOutFailed' && + err.message.includes('Failed to sign out from SageMaker Unified Studio') + ) + } + ) + }) + + it('should throw ToolkitError when forgetConnection fails', async function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + mockState.get.withArgs('smus.connections').returns({}) + mockSecondaryAuth.forgetConnection.rejects(new Error('Forget failed')) + + await assert.rejects( + () => smusAuthProvider.signOut(), + (err: ToolkitError) => { + return ( + err.code === 'SignOutFailed' && + err.message.includes('Failed to sign out from SageMaker Unified Studio') + ) + } + ) + }) + }) + + describe('connectWithIamProfile', function () { + let mockState: any + const testProfileName = 'test-profile' + const testIamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + } + + beforeEach(function () { + mockState = { + get: sinon.stub(), + update: sinon.stub().resolves(), + } + mockSecondaryAuth.state = mockState + mockAuth.getConnection = sinon.stub() + mockAuth.refreshConnectionState = sinon.stub().resolves() + }) + + it('should connect with existing IAM profile and store metadata', async function () { + extractDomainInfoStub.returns({ domainId: testDomainId, region: testRegion }) + mockAuth.getConnection.withArgs({ id: `profile:${testProfileName}` }).resolves(testIamConnection) + mockState.get.withArgs('smus.connections').returns({}) + + const result = await smusAuthProvider.connectWithIamProfile(testProfileName, testRegion, testDomainUrl) + + assert.strictEqual(result.id, testIamConnection.id) + assert.strictEqual(result.type, 'iam') + assert.strictEqual(result.profileName, testProfileName) + assert.strictEqual(result.region, testRegion) + assert.strictEqual(result.domainUrl, testDomainUrl) + assert.strictEqual(result.domainId, testDomainId) + + assert.ok(mockAuth.getConnection.calledWith({ id: `profile:${testProfileName}` })) + assert.ok(mockSecondaryAuth.useNewConnection.calledWith(testIamConnection)) + assert.ok(mockAuth.refreshConnectionState.calledWith(testIamConnection)) + assert.ok( + mockState.update.calledWith('smus.connections', { + [testIamConnection.id]: { + profileName: testProfileName, + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + isIamDomain: false, + }, + }) + ) + }) + + it('should merge with existing SMUS connections metadata', async function () { + extractDomainInfoStub.returns({ domainId: testDomainId, region: testRegion }) + mockAuth.getConnection.withArgs({ id: `profile:${testProfileName}` }).resolves(testIamConnection) + + const existingConnections = { + 'other-connection-id': { + domainUrl: 'https://other-domain.sagemaker.us-west-2.on.aws', + domainId: 'other-domain-id', + }, + } + mockState.get.withArgs('smus.connections').returns(existingConnections) + + await smusAuthProvider.connectWithIamProfile(testProfileName, testRegion, testDomainUrl) + + assert.ok( + mockState.update.calledWith('smus.connections', { + 'other-connection-id': existingConnections['other-connection-id'], + [testIamConnection.id]: { + profileName: testProfileName, + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + isIamDomain: false, + }, + }) + ) + }) + + it('should throw error for invalid domain URL', async function () { + extractDomainInfoStub.returns({ domainId: undefined, region: testRegion }) + + await assert.rejects( + () => smusAuthProvider.connectWithIamProfile(testProfileName, testRegion, 'invalid-url'), + (err: ToolkitError) => { + return ( + err.code === 'FailedToConnect' && + err.message.includes('Failed to connect to SageMaker Unified Studio with IAM profile') + ) + } + ) + + assert.ok(mockAuth.getConnection.notCalled) + assert.ok(mockSecondaryAuth.useNewConnection.notCalled) + }) + + it('should throw error when IAM connection not found', async function () { + extractDomainInfoStub.returns({ domainId: testDomainId, region: testRegion }) + mockAuth.getConnection.withArgs({ id: `profile:${testProfileName}` }).resolves(undefined) + + await assert.rejects( + () => smusAuthProvider.connectWithIamProfile(testProfileName, testRegion, testDomainUrl), + (err: ToolkitError) => { + return ( + err.code === 'FailedToConnect' && + err.message.includes('Failed to connect to SageMaker Unified Studio with IAM profile') && + (err.cause as any)?.code === 'ConnectionNotFound' + ) + } + ) + + assert.ok(mockSecondaryAuth.useNewConnection.notCalled) + }) + + it('should throw error when connection is not IAM type', async function () { + extractDomainInfoStub.returns({ domainId: testDomainId, region: testRegion }) + const nonIamConnection = { + id: 'profile:test-profile', + type: 'sso' as const, + label: 'Test SSO Connection', + } + mockAuth.getConnection.withArgs({ id: `profile:${testProfileName}` }).resolves(nonIamConnection) + + await assert.rejects( + () => smusAuthProvider.connectWithIamProfile(testProfileName, testRegion, testDomainUrl), + (err: ToolkitError) => { + return ( + err.code === 'FailedToConnect' && + err.message.includes('Failed to connect to SageMaker Unified Studio with IAM profile') + ) + } + ) + }) + + it('should handle useNewConnection failure', async function () { + extractDomainInfoStub.returns({ domainId: testDomainId, region: testRegion }) + mockAuth.getConnection.withArgs({ id: `profile:${testProfileName}` }).resolves(testIamConnection) + mockState.get.withArgs('smus.connections').returns({}) + mockSecondaryAuth.useNewConnection.rejects(new Error('Failed to use connection')) + + await assert.rejects( + () => smusAuthProvider.connectWithIamProfile(testProfileName, testRegion, testDomainUrl), + (err: ToolkitError) => { + return ( + err.code === 'FailedToConnect' && + err.message.includes('Failed to connect to SageMaker Unified Studio with IAM profile') + ) + } + ) + }) + + it('should handle refreshConnectionState failure', async function () { + extractDomainInfoStub.returns({ domainId: testDomainId, region: testRegion }) + mockAuth.getConnection.withArgs({ id: `profile:${testProfileName}` }).resolves(testIamConnection) + mockState.get.withArgs('smus.connections').returns({}) + mockAuth.refreshConnectionState.rejects(new Error('Failed to refresh state')) + + await assert.rejects( + () => smusAuthProvider.connectWithIamProfile(testProfileName, testRegion, testDomainUrl), + (err: ToolkitError) => { + return ( + err.code === 'FailedToConnect' && + err.message.includes('Failed to connect to SageMaker Unified Studio with IAM profile') + ) + } + ) + }) + }) + + describe('activeConnection with IAM metadata', function () { + let mockState: any + + beforeEach(function () { + mockState = { + get: sinon.stub(), + update: sinon.stub().resolves(), + } + mockSecondaryAuth.state = mockState + }) + + it('should return IAM connection with SMUS metadata when available', function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + const smusConnections = { + 'profile:test-profile': { + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + }, + } + mockState.get.withArgs('smus.connections').returns(smusConnections) + + const result = smusAuthProvider.activeConnection + + assert.strictEqual(result?.id, iamConnection.id) + assert.strictEqual((result as any)?.type, 'iam') + assert.strictEqual((result as any).profileName, 'test-profile') + assert.strictEqual((result as any).region, testRegion) + assert.strictEqual((result as any).domainUrl, testDomainUrl) + assert.strictEqual((result as any).domainId, testDomainId) + }) + + it('should return SSO connection with SMUS metadata when available', function () { + const ssoConnection = { + ...mockSmusConnection, + type: 'sso' as const, + } + mockSecondaryAuthState.activeConnection = ssoConnection + + const smusConnections = { + [ssoConnection.id]: { + domainUrl: testDomainUrl, + domainId: testDomainId, + }, + } + mockState.get.withArgs('smus.connections').returns(smusConnections) + + const result = smusAuthProvider.activeConnection + + assert.strictEqual(result?.id, ssoConnection.id) + assert.strictEqual((result as any)?.type, 'sso') + assert.strictEqual((result as any)?.domainUrl, testDomainUrl) + assert.strictEqual((result as any)?.domainId, testDomainId) + }) + + it('should return base connection when no metadata available', function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + mockState.get.withArgs('smus.connections').returns({}) + + const result = smusAuthProvider.activeConnection + + assert.strictEqual(result?.id, iamConnection.id) + assert.strictEqual((result as any)?.type, 'iam') + assert.strictEqual((result as any).profileName, undefined) + assert.strictEqual((result as any).domainUrl, undefined) + }) + + it('should return undefined when no active connection', function () { + mockSecondaryAuthState.activeConnection = undefined + + const result = smusAuthProvider.activeConnection + + assert.strictEqual(result, undefined) + }) + + it('should handle missing smus.connections state gracefully', function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + mockState.get.withArgs('smus.connections').returns(undefined) + + const result = smusAuthProvider.activeConnection + + assert.strictEqual(result?.id, iamConnection.id) + assert.strictEqual((result as any)?.type, 'iam') + }) + }) + + describe('getDerCredentialsProvider', function () { + let getContextStub: sinon.SinonStub + + beforeEach(function () { + getContextStub = sinon.stub(vscodeSetContext, 'getContext') + + // Clear cache + smusAuthProvider['credentialsProviderCache'].clear() + }) + + describe('in SMUS space environment', function () { + beforeEach(function () { + getContextStub.withArgs('aws.smus.inSmusSpaceEnvironment').returns(true) + + // Mock resource metadata for SMUS space environment + getResourceMetadataStub = sinon.stub(resourceMetadataUtils, 'getResourceMetadata').returns({ + ResourceArn: 'arn:aws:sagemaker:us-east-2:123456789012:app/dzd_domainId/test-app', + AdditionalMetadata: { + DataZoneDomainId: testDomainId, + DataZoneDomainRegion: testRegion, + }, + } as any) + }) + + afterEach(function () { + getResourceMetadataStub?.restore() + }) + + it('should return a credentials provider that can retrieve credentials', async function () { + // In SMUS space environment, the method should return a provider + // We can't easily test the internal branching logic without stubbing ES modules + // So we test that it returns a valid provider structure + const provider = await smusAuthProvider.getDerCredentialsProvider() + + assert.ok(provider, 'Provider should be returned') + assert.ok(typeof provider.getCredentials === 'function', 'Provider should have getCredentials method') + }) + + it('should not cache providers in SMUS space environment', async function () { + // Get provider twice + const provider1 = await smusAuthProvider.getDerCredentialsProvider() + const provider2 = await smusAuthProvider.getDerCredentialsProvider() + + // In SMUS space, providers are not cached (new provider each time) + // This is because the logic returns early before caching + assert.ok(provider1) + assert.ok(provider2) + }) + }) + + describe('in non-SMUS space environment', function () { + let getAccessTokenStub: sinon.SinonStub + + beforeEach(function () { + getContextStub.withArgs('aws.smus.inSmusSpaceEnvironment').returns(false) + mockSecondaryAuthState.activeConnection = mockSmusConnection + getAccessTokenStub = sinon.stub(smusAuthProvider, 'getAccessToken').resolves('mock-access-token') + }) + + it('should create and cache DomainExecRoleCredentialsProvider for SSO connection', async function () { + const provider = await smusAuthProvider.getDerCredentialsProvider() + + assert.ok(provider) + assert.ok(getAccessTokenStub.notCalled) // Not called until getCredentials is invoked + + // Verify caching + const cachedProvider = await smusAuthProvider.getDerCredentialsProvider() + assert.strictEqual(provider, cachedProvider) + }) + + it('should throw error when no active connection', async function () { + mockSecondaryAuthState.activeConnection = undefined + + await assert.rejects( + () => smusAuthProvider.getDerCredentialsProvider(), + (err: ToolkitError) => { + return ( + err.code === 'NoActiveConnection' && + err.message.includes('No active SMUS connection available') + ) + } + ) + }) + + it('should throw error for non-SSO connection', async function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + await assert.rejects( + () => smusAuthProvider.getDerCredentialsProvider(), + (err: ToolkitError) => { + return ( + err.code === 'InvalidConnectionType' && + err.message.includes( + 'Domain Execution Role credentials are only available for SSO connections' + ) + ) + } + ) + }) + + it('should use cached provider for same connection', async function () { + const provider1 = await smusAuthProvider.getDerCredentialsProvider() + const provider2 = await smusAuthProvider.getDerCredentialsProvider() + + assert.strictEqual(provider1, provider2) + }) + + it('should create different providers for different connections', async function () { + const provider1 = await smusAuthProvider.getDerCredentialsProvider() + + // Change connection + const differentConnection = { + ...mockSmusConnection, + id: 'different-connection-id', + domainId: 'different-domain-id', + } + mockSecondaryAuthState.activeConnection = differentConnection + + const provider2 = await smusAuthProvider.getDerCredentialsProvider() + + assert.notStrictEqual(provider1, provider2) + }) + }) + }) + + describe('initIamModeContextInSpaceEnvironment', function () { + let getResourceMetadataStub: sinon.SinonStub + let getDerCredentialsProviderStub: sinon.SinonStub + let getInstanceStub: sinon.SinonStub + let mockCredentialsProvider: any + let mockClientHelper: any + + const testResourceMetadata = { + AdditionalMetadata: { + DataZoneDomainId: 'test-domain-id', + DataZoneDomainRegion: 'us-east-1', + DataZoneProjectId: 'test-project-id', + }, + } + + beforeEach(function () { + getResourceMetadataStub = sinon.stub(resourceMetadataUtils, 'getResourceMetadata') + + // Reset the global setContext stub history for clean test state + setContextStubGlobal.resetHistory() + + mockCredentialsProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + } + + getDerCredentialsProviderStub = sinon + .stub(smusAuthProvider, 'getDerCredentialsProvider') + .resolves(mockCredentialsProvider) + + // Mock DataZoneCustomClientHelper + const getDomainStub = sinon.stub() + mockClientHelper = { + getDomain: getDomainStub, + } + + getInstanceStub = sinon.stub(DataZoneCustomClientHelper, 'getInstance').returns(mockClientHelper) + + // Setup getDomain to return domain details + getDomainStub.resolves({ + id: testResourceMetadata.AdditionalMetadata.DataZoneDomainId, + domainVersion: 'V2', + iamSignIns: ['IAM_ROLE', 'IAM_USER'], + }) + }) + + afterEach(function () { + sinon.restore() + }) + + it('should set IAM mode context to true when domain is IAM mode', async function () { + getResourceMetadataStub.returns(testResourceMetadata) + + await smusAuthProvider['initIamModeContextInSpaceEnvironment']() + + assert.ok(getResourceMetadataStub.called) + assert.ok(getDerCredentialsProviderStub.called) + assert.ok( + getInstanceStub.calledWith( + mockCredentialsProvider, + testResourceMetadata.AdditionalMetadata.DataZoneDomainRegion + ) + ) + assert.ok(setContextStubGlobal.calledWith('aws.smus.isIamMode', true)) + }) + + it('should set IAM mode context to false when domain is not IAM mode', async function () { + getResourceMetadataStub.returns(testResourceMetadata) + + // Override getDomain to return a non-IAM domain + mockClientHelper.getDomain = sinon.stub().resolves({ + id: testResourceMetadata.AdditionalMetadata.DataZoneDomainId, + domainVersion: 'V2', + }) + + await smusAuthProvider['initIamModeContextInSpaceEnvironment']() + + assert.ok(getResourceMetadataStub.called) + assert.ok(getDerCredentialsProviderStub.called) + assert.ok( + getInstanceStub.calledWith( + mockCredentialsProvider, + testResourceMetadata.AdditionalMetadata.DataZoneDomainRegion + ) + ) + assert.ok(setContextStubGlobal.calledWith('aws.smus.isIamMode', false)) + }) + + it('should not call IAM mode check when resource metadata is missing', async function () { + getResourceMetadataStub.returns(undefined) + + await smusAuthProvider['initIamModeContextInSpaceEnvironment']() + + assert.ok(getResourceMetadataStub.called) + assert.ok(getDerCredentialsProviderStub.notCalled) + assert.ok(getInstanceStub.notCalled) + assert.ok(setContextStubGlobal.notCalled) + }) + + it('should handle error when getDerCredentialsProvider fails', async function () { + getResourceMetadataStub.returns(testResourceMetadata) + const testError = new Error('Failed to get credentials provider') + getDerCredentialsProviderStub.rejects(testError) + + await smusAuthProvider['initIamModeContextInSpaceEnvironment']() + + assert.ok(getResourceMetadataStub.called) + assert.ok(getDerCredentialsProviderStub.called) + assert.ok(getInstanceStub.notCalled) + assert.ok(setContextStubGlobal.calledWith('aws.smus.isIamMode', false)) + }) + }) + + describe('getSessionName', function () { + let mockStsClient: any + let mockCredentialsProvider: any + + beforeEach(function () { + // Mock STS client + mockStsClient = { + getCallerIdentity: sinon.stub(), + } + sinon + .stub(DefaultStsClient.prototype, 'getCallerIdentity') + .callsFake(() => mockStsClient.getCallerIdentity()) + + // Mock credentials provider + mockCredentialsProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-access-key', + secretAccessKey: 'test-secret-key', + sessionToken: 'test-session-token', + }), + } + + sinon + .stub(smusAuthProvider as any, 'getCredentialsForIamProfile') + .resolves(mockCredentialsProvider.getCredentials()) + }) + + afterEach(function () { + sinon.restore() + }) + + it('should return session name for IAM connection with assumed role', async function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + endpointUrl: undefined, + getCredentials: sinon.stub().resolves(), + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + // Mock STS response with assumed role ARN + const assumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/MyRole/my-session-name' + mockStsClient.getCallerIdentity.resolves({ + Arn: assumedRoleArn, + Account: '123456789012', + UserId: 'AIDAI1234567890EXAMPLE:my-session-name', + }) + + // Mock connection metadata + const smusConnections = { + [iamConnection.id]: { + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + }, + } + mockSecondaryAuth.state.get.withArgs('smus.connections').returns(smusConnections) + + const sessionName = await smusAuthProvider.getSessionName() + + assert.strictEqual(sessionName, 'my-session-name') + assert.ok(mockStsClient.getCallerIdentity.calledOnce) + }) + + it('should return undefined for IAM connection without assumed role (IAM user)', async function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + endpointUrl: undefined, + getCredentials: sinon.stub().resolves(), + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + // Mock STS response with IAM user ARN (no session name) + const iamUserArn = 'arn:aws:iam::123456789012:user/my-user' + mockStsClient.getCallerIdentity.resolves({ + Arn: iamUserArn, + Account: '123456789012', + UserId: 'AIDAI1234567890EXAMPLE', + }) + + // Mock connection metadata + const smusConnections = { + [iamConnection.id]: { + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + }, + } + mockSecondaryAuth.state.get.withArgs('smus.connections').returns(smusConnections) + + const sessionName = await smusAuthProvider.getSessionName() + + assert.strictEqual(sessionName, undefined) + assert.ok(mockStsClient.getCallerIdentity.calledOnce) + }) + + it('should return undefined for SSO connection', async function () { + mockSecondaryAuthState.activeConnection = mockSmusConnection + + const sessionName = await smusAuthProvider.getSessionName() + + assert.strictEqual(sessionName, undefined) + assert.ok(mockStsClient.getCallerIdentity.notCalled) + }) + + it('should return undefined when not connected', async function () { + mockSecondaryAuthState.activeConnection = undefined + + const sessionName = await smusAuthProvider.getSessionName() + + assert.strictEqual(sessionName, undefined) + assert.ok(mockStsClient.getCallerIdentity.notCalled) + }) + + it('should cache and reuse caller identity ARN', async function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + endpointUrl: undefined, + getCredentials: sinon.stub().resolves(), + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + const assumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/MyRole/my-session-name' + mockStsClient.getCallerIdentity.resolves({ + Arn: assumedRoleArn, + Account: '123456789012', + UserId: 'AIDAI1234567890EXAMPLE:my-session-name', + }) + + // Mock connection metadata + const smusConnections = { + [iamConnection.id]: { + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + }, + } + mockSecondaryAuth.state.get.withArgs('smus.connections').returns(smusConnections) + + // First call - should fetch from STS + const sessionName1 = await smusAuthProvider.getSessionName() + assert.strictEqual(sessionName1, 'my-session-name') + assert.ok(mockStsClient.getCallerIdentity.calledOnce) + + // Second call - should use cached value + const sessionName2 = await smusAuthProvider.getSessionName() + assert.strictEqual(sessionName2, 'my-session-name') + assert.ok(mockStsClient.getCallerIdentity.calledOnce) // Still only called once + }) + + it('should handle STS errors gracefully', async function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + endpointUrl: undefined, + getCredentials: sinon.stub().resolves(), + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + mockStsClient.getCallerIdentity.rejects(new Error('STS call failed')) + + // Mock connection metadata + const smusConnections = { + [iamConnection.id]: { + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + }, + } + mockSecondaryAuth.state.get.withArgs('smus.connections').returns(smusConnections) + + const sessionName = await smusAuthProvider.getSessionName() + + assert.strictEqual(sessionName, undefined) + }) + + it('should return undefined when connection metadata is missing', async function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + endpointUrl: undefined, + getCredentials: sinon.stub().resolves(), + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + // No connection metadata + mockSecondaryAuth.state.get.withArgs('smus.connections').returns({}) + + const sessionName = await smusAuthProvider.getSessionName() + + assert.strictEqual(sessionName, undefined) + assert.ok(mockStsClient.getCallerIdentity.notCalled) + }) + }) + + describe('getRoleArn', function () { + let mockStsClient: any + let mockCredentialsProvider: any + + beforeEach(function () { + // Mock STS client + mockStsClient = { + getCallerIdentity: sinon.stub(), + } + sinon + .stub(DefaultStsClient.prototype, 'getCallerIdentity') + .callsFake(() => mockStsClient.getCallerIdentity()) + + // Mock credentials provider + mockCredentialsProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-access-key', + secretAccessKey: 'test-secret-key', + sessionToken: 'test-session-token', + }), + } + + sinon + .stub(smusAuthProvider as any, 'getCredentialsForIamProfile') + .resolves(mockCredentialsProvider.getCredentials()) + }) + + afterEach(function () { + sinon.restore() + }) + + it('should return IAM role ARN for IAM connection with assumed role', async function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + endpointUrl: undefined, + getCredentials: sinon.stub().resolves(), + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + // Mock STS response with assumed role ARN + const assumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/MyRole/my-session-name' + mockStsClient.getCallerIdentity.resolves({ + Arn: assumedRoleArn, + Account: '123456789012', + UserId: 'AIDAI1234567890EXAMPLE:my-session-name', + }) + + // Mock connection metadata + const smusConnections = { + [iamConnection.id]: { + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + }, + } + mockSecondaryAuth.state.get.withArgs('smus.connections').returns(smusConnections) + + const roleArn = await smusAuthProvider.getIamPrincipalArn() + + // Should convert assumed role ARN to IAM role ARN + assert.strictEqual(roleArn, 'arn:aws:iam::123456789012:role/MyRole') + assert.ok(mockStsClient.getCallerIdentity.calledOnce) + }) + + it('should return undefined for SSO connection', async function () { + mockSecondaryAuthState.activeConnection = mockSmusConnection + + const roleArn = await smusAuthProvider.getIamPrincipalArn() + + assert.strictEqual(roleArn, undefined) + assert.ok(mockStsClient.getCallerIdentity.notCalled) + }) + + it('should return undefined when not connected', async function () { + mockSecondaryAuthState.activeConnection = undefined + + const roleArn = await smusAuthProvider.getIamPrincipalArn() + + assert.strictEqual(roleArn, undefined) + assert.ok(mockStsClient.getCallerIdentity.notCalled) + }) + + it('should use cached caller identity ARN', async function () { + const iamConnection = { + id: 'profile:test-profile', + type: 'iam' as const, + label: 'Test IAM Profile', + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + endpointUrl: undefined, + getCredentials: sinon.stub().resolves(), + } + mockSecondaryAuthState.activeConnection = iamConnection as any + + const assumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/MyRole/my-session-name' + mockStsClient.getCallerIdentity.resolves({ + Arn: assumedRoleArn, + Account: '123456789012', + UserId: 'AIDAI1234567890EXAMPLE:my-session-name', + }) + + // Mock connection metadata + const smusConnections = { + [iamConnection.id]: { + profileName: 'test-profile', + region: testRegion, + domainUrl: testDomainUrl, + domainId: testDomainId, + }, + } + mockSecondaryAuth.state.get.withArgs('smus.connections').returns(smusConnections) + + // First call - should fetch from STS + const roleArn1 = await smusAuthProvider.getIamPrincipalArn() + assert.strictEqual(roleArn1, 'arn:aws:iam::123456789012:role/MyRole') + assert.ok(mockStsClient.getCallerIdentity.calledOnce) + + // Second call - should use cached value + const roleArn2 = await smusAuthProvider.getIamPrincipalArn() + assert.strictEqual(roleArn2, 'arn:aws:iam::123456789012:role/MyRole') + assert.ok(mockStsClient.getCallerIdentity.calledOnce) // Still only called once + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/auth/ui/authenticationMethodSelection.test.ts b/packages/core/src/test/sagemakerunifiedstudio/auth/ui/authenticationMethodSelection.test.ts new file mode 100644 index 00000000000..e0332326be1 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/ui/authenticationMethodSelection.test.ts @@ -0,0 +1,39 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { SmusAuthenticationMethodSelector } from '../../../../sagemakerunifiedstudio/auth/ui/authenticationMethodSelection' + +describe('SmusAuthenticationMethodSelector', function () { + // Note: Due to AWS Toolkit test framework restrictions on mocking vscode.window, + // these tests focus on the interface and behavior rather than deep mocking. + // The actual QuickPick functionality is tested through integration tests. + + describe('showAuthenticationMethodSelection', function () { + it('should export the correct interface', function () { + // Verify the class exists and has the expected static method + assert.ok('showAuthenticationMethodSelection' in SmusAuthenticationMethodSelector) + assert.strictEqual(typeof SmusAuthenticationMethodSelector.showAuthenticationMethodSelection, 'function') + }) + + it('should handle authentication method types correctly', function () { + // Test that the types are properly defined + const testMethod1: 'sso' | 'iam' = 'sso' + const testMethod2: 'sso' | 'iam' = 'iam' + + assert.strictEqual(testMethod1, 'sso') + assert.strictEqual(testMethod2, 'iam') + }) + + // The actual UI testing would be done manually or through E2E tests + it('should be callable without throwing', function () { + // Verify the method exists and is accessible + assert.doesNotThrow(() => { + // Just verify the method exists without calling it + assert.ok('showAuthenticationMethodSelection' in SmusAuthenticationMethodSelector) + }) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/auth/ui/iamProfileSelection.test.ts b/packages/core/src/test/sagemakerunifiedstudio/auth/ui/iamProfileSelection.test.ts new file mode 100644 index 00000000000..2a56b2baf0c --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/ui/iamProfileSelection.test.ts @@ -0,0 +1,21 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { SmusIamProfileSelector } from '../../../../sagemakerunifiedstudio/auth/ui/iamProfileSelection' + +describe('SmusIamProfileSelector', function () { + describe('showRegionSelection', function () { + it('should be a static method', function () { + assert.strictEqual(typeof SmusIamProfileSelector.showRegionSelection, 'function') + }) + }) + + describe('showIamProfileSelection', function () { + it('should be a static method', function () { + assert.strictEqual(typeof SmusIamProfileSelector.showIamProfileSelection, 'function') + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/auth/ui/ssoAuthentication.test.ts b/packages/core/src/test/sagemakerunifiedstudio/auth/ui/ssoAuthentication.test.ts new file mode 100644 index 00000000000..6a4221ac7a4 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/ui/ssoAuthentication.test.ts @@ -0,0 +1,40 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { SmusSsoAuthenticationUI } from '../../../../sagemakerunifiedstudio/auth/ui/ssoAuthentication' + +describe('SmusSsoAuthenticationUI', function () { + // Note: Due to AWS Toolkit test framework restrictions on mocking vscode.window, + // these tests focus on the interface and behavior rather than deep mocking. + // The actual QuickPick functionality is tested through integration tests. + + describe('showDomainUrlInput', function () { + it('should export the correct interface', function () { + // Verify the class exists and has the expected static method + assert.ok('showDomainUrlInput' in SmusSsoAuthenticationUI) + assert.strictEqual(typeof SmusSsoAuthenticationUI.showDomainUrlInput, 'function') + }) + + it('should be callable without throwing', function () { + // Verify the method exists and is accessible + assert.doesNotThrow(() => { + // Just verify the method exists without calling it + assert.ok('showDomainUrlInput' in SmusSsoAuthenticationUI) + }) + }) + + it('should handle return type union correctly', function () { + // Test that the return types are properly defined + const testResult1: string | 'BACK' | undefined = 'https://example.com' + const testResult2: string | 'BACK' | undefined = 'BACK' + const testResult3: string | 'BACK' | undefined = undefined + + assert.strictEqual(testResult1, 'https://example.com') + assert.strictEqual(testResult2, 'BACK') + assert.strictEqual(testResult3, undefined) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/connectionMagicsSelector/activation.test.ts b/packages/core/src/test/sagemakerunifiedstudio/connectionMagicsSelector/activation.test.ts new file mode 100644 index 00000000000..86e37c76444 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/connectionMagicsSelector/activation.test.ts @@ -0,0 +1,11 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'assert' + +describe('Connection magic selector test', function () { + it('example test', function () { + assert.ok(true) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/activation.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/activation.test.ts new file mode 100644 index 00000000000..eb770f6bf82 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/activation.test.ts @@ -0,0 +1,579 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import * as vscode from 'vscode' +import { activate } from '../../../sagemakerunifiedstudio/explorer/activation' +import { + SmusAuthenticationProvider, + setSmusConnectedContext, +} from '../../../sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider' +import { ResourceTreeDataProvider } from '../../../shared/treeview/resourceTreeDataProvider' +import { SageMakerUnifiedStudioRootNode } from '../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioRootNode' +import { getLogger } from '../../../shared/logger/logger' +import { getTestWindow } from '../../shared/vscode/window' +import { SeverityLevel } from '../../shared/vscode/message' +import * as extensionUtilities from '../../../shared/extensionUtilities' +import { createMockSpaceNode } from '../testUtils' +import { DataZoneClient } from '../../../sagemakerunifiedstudio/shared/client/datazoneClient' +import * as model from '../../../sagemakerunifiedstudio/auth/model' + +describe('SMUS Explorer Activation', function () { + let mockExtensionContext: vscode.ExtensionContext + let mockSmusAuthProvider: sinon.SinonStubbedInstance + let mockTreeView: sinon.SinonStubbedInstance> + let mockTreeDataProvider: sinon.SinonStubbedInstance + let mockSmusRootNode: sinon.SinonStubbedInstance + let mockProjectNode: any + let createTreeViewStub: sinon.SinonStub + let registerCommandStub: sinon.SinonStub + let executeCommandSpy: sinon.SinonSpy + let dataZoneDisposeStub: sinon.SinonStub + let setupUserActivityMonitoringStub: sinon.SinonStub + + beforeEach(async function () { + mockExtensionContext = { + subscriptions: [], + } as any + + mockSmusAuthProvider = { + restore: sinon.stub().resolves(), + isConnected: sinon.stub().returns(true), + reauthenticate: sinon.stub().resolves(), + onDidChange: sinon.stub().callsFake((_listener: () => void) => ({ dispose: sinon.stub() })), + onDidChangeActiveConnection: sinon.stub().callsFake((_listener: () => void) => ({ dispose: sinon.stub() })), + activeConnection: { + id: 'test-connection', + domainId: 'test-domain', + ssoRegion: 'us-east-1', + }, + getDomainAccountId: sinon.stub().resolves('123456789012'), + } as any + + mockTreeView = { + dispose: sinon.stub(), + } as any + + mockTreeDataProvider = { + refresh: sinon.stub(), + } as any + + mockProjectNode = { + getProject: sinon.stub().returns({ id: 'test-project', name: 'Test Project' }), + refreshNode: sinon.stub().resolves(), + } + + mockSmusRootNode = { + getChildren: sinon.stub().resolves([]), + getProjectSelectNode: sinon.stub().returns(mockProjectNode), + } as any + + // Stub vscode APIs + createTreeViewStub = sinon.stub(vscode.window, 'createTreeView').returns(mockTreeView as any) + registerCommandStub = sinon.stub(vscode.commands, 'registerCommand').returns({ dispose: sinon.stub() } as any) + executeCommandSpy = sinon.spy(vscode.commands, 'executeCommand') + + // Stub SmusAuthenticationProvider + sinon.stub(SmusAuthenticationProvider, 'fromContext').returns(mockSmusAuthProvider as any) + + // Stub DataZoneClient.dispose + dataZoneDisposeStub = sinon.stub(DataZoneClient, 'dispose') + + // Stub SageMakerUnifiedStudioRootNode constructor + sinon.stub(SageMakerUnifiedStudioRootNode.prototype, 'getChildren').returns(mockSmusRootNode.getChildren()) + sinon + .stub(SageMakerUnifiedStudioRootNode.prototype, 'getProjectSelectNode') + .returns(mockSmusRootNode.getProjectSelectNode()) + + // Stub ResourceTreeDataProvider constructor + sinon.stub(ResourceTreeDataProvider.prototype, 'refresh').value(mockTreeDataProvider.refresh) + + // Stub logger + sinon.stub({ getLogger }, 'getLogger').returns({ + debug: sinon.stub(), + info: sinon.stub(), + error: sinon.stub(), + } as any) + + // Stub setSmusConnectedContext + sinon.stub({ setSmusConnectedContext }, 'setSmusConnectedContext').resolves() + + // Stub setupUserActivityMonitoring + setupUserActivityMonitoringStub = sinon + .stub(require('../../../awsService/sagemaker/sagemakerSpace'), 'setupUserActivityMonitoring') + .resolves() + + // Stub isSageMaker to return true for SMUS + sinon.stub(extensionUtilities, 'isSageMaker').returns(true) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('activate', function () { + it('should initialize SMUS authentication provider and call restore', async function () { + await activate(mockExtensionContext) + + assert.ok((SmusAuthenticationProvider.fromContext as sinon.SinonStub).called) + assert.ok(mockSmusAuthProvider.restore.called) + }) + + it('should create tree view with correct configuration', async function () { + await activate(mockExtensionContext) + + assert.ok(createTreeViewStub.calledWith('aws.smus.rootView')) + const createTreeViewArgs = createTreeViewStub.firstCall.args[1] + assert.ok('treeDataProvider' in createTreeViewArgs) + }) + + it('should register all required commands', async function () { + await activate(mockExtensionContext) + + // Check that commands are registered + const registeredCommands = registerCommandStub.getCalls().map((call) => call.args[0]) + + assert.ok(registeredCommands.includes('aws.smus.rootView.refresh')) + assert.ok(registeredCommands.includes('aws.smus.projectView')) + assert.ok(registeredCommands.includes('aws.smus.refreshProject')) + assert.ok(registeredCommands.includes('aws.smus.switchProject')) + assert.ok(registeredCommands.includes('aws.smus.stopSpace')) + assert.ok(registeredCommands.includes('aws.smus.openRemoteConnection')) + assert.ok(registeredCommands.includes('aws.smus.reauthenticate')) + }) + + it('should add all disposables to extension context subscriptions', async function () { + await activate(mockExtensionContext) + + // Should have multiple subscriptions added + assert.ok(mockExtensionContext.subscriptions.length > 0) + }) + + it('should refresh tree data provider on initialization', async function () { + await activate(mockExtensionContext) + + assert.ok(mockTreeDataProvider.refresh.called) + }) + + it('should register DataZone client disposal', async function () { + await activate(mockExtensionContext) + + // Find the DataZone dispose subscription + const subscriptions = mockExtensionContext.subscriptions + assert.ok(subscriptions.length > 0) + + // The DataZone dispose subscription should be among the subscriptions + let dataZoneDisposeFound = false + for (const subscription of subscriptions) { + if (subscription && typeof subscription.dispose === 'function') { + // Try calling dispose and see if it calls DataZoneClient.dispose + const callCountBefore = dataZoneDisposeStub.callCount + subscription.dispose() + if (dataZoneDisposeStub.callCount > callCountBefore) { + dataZoneDisposeFound = true + break + } + } + } + + assert.ok(dataZoneDisposeFound, 'Should register DataZone client disposal') + }) + + describe('command handlers', function () { + beforeEach(async function () { + await activate(mockExtensionContext) + }) + + it('should handle aws.smus.rootView.refresh command', async function () { + const refreshCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.rootView.refresh') + + assert.ok(refreshCommand) + + // Execute the command handler + await refreshCommand.args[1]() + + assert.ok(mockTreeDataProvider.refresh.called) + }) + + it('should handle aws.smus.reauthenticate command with connection', async function () { + const reauthCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.reauthenticate') + + assert.ok(reauthCommand) + + const mockConnection = { + id: 'test-connection', + type: 'sso', + startUrl: 'https://identitycenter.amazonaws.com/ssoins-testInstanceId', + ssoRegion: 'us-east-1', + scopes: ['datazone:domain:access'], + label: 'Test Connection', + } as any + + const testWindow = getTestWindow() + + // Execute the command handler with connection + await reauthCommand.args[1](mockConnection) + + assert.ok(mockSmusAuthProvider.reauthenticate.calledWith(mockConnection)) + assert.ok(mockSmusRootNode.getProjectSelectNode.called) + assert.ok((mockProjectNode.getProject as sinon.SinonStub).called) + assert.ok(executeCommandSpy.neverCalledWith('aws.smus.switchProject')) + assert.ok(mockTreeDataProvider.refresh.called) + + // Check that an information message was shown + const infoMessages = testWindow.shownMessages.filter( + (msg) => msg.severity === SeverityLevel.Information + ) + assert.ok(infoMessages.length > 0, 'Should show information message') + assert.ok(infoMessages.some((msg) => msg.message.includes('Successfully reauthenticated'))) + }) + + it('should handle aws.smus.reauthenticate command without connection', async function () { + const reauthCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.reauthenticate') + + assert.ok(reauthCommand) + + // Execute the command handler without connection + await reauthCommand.args[1]() + + assert.ok(mockSmusAuthProvider.reauthenticate.notCalled) + }) + + it('should handle reauthentication errors', async function () { + const reauthCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.reauthenticate') + + assert.ok(reauthCommand) + + const mockConnection = { + id: 'test-connection', + type: 'sso', + startUrl: 'https://identitycenter.amazonaws.com/ssoins-testInstanceId', + ssoRegion: 'us-east-1', + scopes: ['datazone:domain:access'], + label: 'Test Connection', + } as any + const error = new Error('Reauthentication failed') + mockSmusAuthProvider.reauthenticate.rejects(error) + + const testWindow = getTestWindow() + + // Execute the command handler + await reauthCommand.args[1](mockConnection) + + // Check that an error message was shown + const errorMessages = testWindow.shownMessages.filter((msg) => msg.severity === SeverityLevel.Error) + assert.ok(errorMessages.length > 0, 'Should show error message') + assert.ok(errorMessages.some((msg) => msg.message.includes('Reauthentication failed'))) + }) + + it('should extract detailed error message from ToolkitError cause chain', async function () { + const reauthCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.reauthenticate') + + assert.ok(reauthCommand) + + const mockConnection = { + id: 'test-connection', + type: 'sso', + startUrl: 'https://identitycenter.amazonaws.com/ssoins-testInstanceId', + ssoRegion: 'us-east-1', + scopes: ['datazone:domain:access'], + label: 'Test Connection', + } as any + + // Create a ToolkitError with a cause chain + const detailedError = new Error('Invalid profile - The security token is expired') + const wrapperError = new Error('Unable to reauthenticate SageMaker Unified Studio connection.') + ;(wrapperError as any).cause = detailedError + mockSmusAuthProvider.reauthenticate.rejects(wrapperError) + + const testWindow = getTestWindow() + + // Execute the command handler + await reauthCommand.args[1](mockConnection) + + // Check that the detailed error message from the cause was shown + const errorMessages = testWindow.shownMessages.filter((msg) => msg.severity === SeverityLevel.Error) + assert.ok(errorMessages.length > 0, 'Should show error message') + const hasDetailedError = errorMessages.some((msg) => + msg.message.includes('Invalid profile - The security token is expired') + ) + assert.ok(hasDetailedError, 'Should show detailed error from cause chain') + }) + + it('should not show success message for IAM connection reauthentication', async function () { + const reauthCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.reauthenticate') + + assert.ok(reauthCommand) + + // Create an IAM connection + const mockIamConnection = { + id: 'test-iam-connection', + type: 'iam', + profileName: 'test-profile', + region: 'us-east-1', + label: 'Test IAM Connection', + } as any + + // Stub isSmusIamConnection to return true for IAM connection + sinon.stub(model, 'isSmusIamConnection').returns(true) + + // Mock the return value to return the connection (IAM connection handled its own message) + mockSmusAuthProvider.reauthenticate.resolves(mockIamConnection) + + const testWindow = getTestWindow() + + // Execute the command handler + await reauthCommand.args[1](mockIamConnection) + + assert.ok(mockSmusAuthProvider.reauthenticate.calledWith(mockIamConnection)) + assert.ok(mockTreeDataProvider.refresh.called) + + // Check that NO information message was shown (IAM handles its own) + const infoMessages = testWindow.shownMessages.filter( + (msg) => msg.severity === SeverityLevel.Information + ) + assert.ok( + !infoMessages.some((msg) => msg.message.includes('Successfully reauthenticated')), + 'Should not show success message for IAM connection' + ) + }) + + it('should show success message for SSO connection reauthentication', async function () { + const reauthCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.reauthenticate') + + assert.ok(reauthCommand) + + const mockSsoConnection = { + id: 'test-sso-connection', + type: 'sso', + startUrl: 'https://identitycenter.amazonaws.com/ssoins-testInstanceId', + ssoRegion: 'us-east-1', + scopes: ['datazone:domain:access'], + label: 'Test SSO Connection', + } as any + + // Stub isSmusIamConnection to return false for SSO connection + sinon.stub(model, 'isSmusIamConnection').returns(false) + + // Mock the return value to indicate SSO connection (returns connection object) + mockSmusAuthProvider.reauthenticate.resolves(mockSsoConnection) + + const testWindow = getTestWindow() + + // Execute the command handler + await reauthCommand.args[1](mockSsoConnection) + + assert.ok(mockSmusAuthProvider.reauthenticate.calledWith(mockSsoConnection)) + assert.ok(mockTreeDataProvider.refresh.called) + + // Check that an information message was shown for SSO + const infoMessages = testWindow.shownMessages.filter( + (msg) => msg.severity === SeverityLevel.Information + ) + assert.ok(infoMessages.length > 0, 'Should show information message for SSO') + assert.ok( + infoMessages.some((msg) => msg.message.includes('Successfully reauthenticated')), + 'Should show success message for SSO connection' + ) + }) + + it('should handle aws.smus.refreshProject command', async function () { + const refreshProjectCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.refreshProject') + + assert.ok(refreshProjectCommand) + + // Execute the command handler + await refreshProjectCommand.args[1]() + + // Verify that getProjectSelectNode was called and refreshNode was called on the returned node + assert.ok(mockSmusRootNode.getProjectSelectNode.called) + const projectNode = mockSmusRootNode.getProjectSelectNode() + assert.ok((projectNode.refreshNode as sinon.SinonStub).called) + }) + + it('should handle aws.smus.stopSpace command with valid node', async function () { + const stopSpaceCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.stopSpace') + + assert.ok(stopSpaceCommand) + + const mockSpaceNode = createMockSpaceNode() + + // Mock the stopSpace function + const stopSpaceStub = sinon.stub() + sinon.stub(require('../../../awsService/sagemaker/commands'), 'stopSpace').value(stopSpaceStub) + + // Execute the command handler + await stopSpaceCommand.args[1](mockSpaceNode) + + assert.ok( + stopSpaceStub.calledWith( + mockSpaceNode.resource, + mockExtensionContext, + mockSpaceNode.resource.sageMakerClient + ) + ) + }) + + it('should handle aws.smus.stopSpace command with invalid node', async function () { + const stopSpaceCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.stopSpace') + + assert.ok(stopSpaceCommand) + + const testWindow = getTestWindow() + + // Execute the command handler with undefined node + await stopSpaceCommand.args[1](undefined) + + // Check that a warning message was shown + const warningMessages = testWindow.shownMessages.filter((msg) => msg.severity === SeverityLevel.Warning) + assert.ok(warningMessages.length > 0, 'Should show warning message') + assert.ok(warningMessages.some((msg) => msg.message.includes('Space information is being refreshed'))) + }) + + it('should handle aws.smus.openRemoteConnection command with valid node', async function () { + const openRemoteCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.openRemoteConnection') + + assert.ok(openRemoteCommand) + + const mockSpaceNode = createMockSpaceNode() + + // Mock the openRemoteConnect function + const openRemoteConnectStub = sinon.stub() + sinon + .stub(require('../../../awsService/sagemaker/commands'), 'openRemoteConnect') + .value(openRemoteConnectStub) + + // Execute the command handler + await openRemoteCommand.args[1](mockSpaceNode) + + assert.ok( + openRemoteConnectStub.calledWith( + mockSpaceNode.resource, + mockExtensionContext, + mockSpaceNode.resource.sageMakerClient + ) + ) + }) + + it('should handle aws.smus.openRemoteConnection command with invalid node', async function () { + const openRemoteCommand = registerCommandStub + .getCalls() + .find((call) => call.args[0] === 'aws.smus.openRemoteConnection') + + assert.ok(openRemoteCommand) + + const testWindow = getTestWindow() + + // Execute the command handler with undefined node + await openRemoteCommand.args[1](undefined) + + // Check that a warning message was shown + const warningMessages = testWindow.shownMessages.filter((msg) => msg.severity === SeverityLevel.Warning) + assert.ok(warningMessages.length > 0, 'Should show warning message') + assert.ok(warningMessages.some((msg) => msg.message.includes('Space information is being refreshed'))) + }) + }) + + it('should propagate auth provider initialization errors', async function () { + const error = new Error('Auth provider initialization failed') + mockSmusAuthProvider.restore.rejects(error) + + // Should throw the error since there's no error handling in activate() + await assert.rejects(() => activate(mockExtensionContext), /Auth provider initialization failed/) + }) + + it('should create root node with auth provider', async function () { + await activate(mockExtensionContext) + + // Verify that SageMakerUnifiedStudioRootNode was created with the auth provider + assert.ok(createTreeViewStub.called) + const treeDataProvider = createTreeViewStub.firstCall.args[1].treeDataProvider + assert.ok(treeDataProvider) + }) + + // TODO: Fix the activation test + it.skip('should setup user activity monitoring', async function () { + await activate(mockExtensionContext) + + assert.ok(setupUserActivityMonitoringStub.called) + }) + }) + + describe('command registration', function () { + it('should register commands with correct names', async function () { + await activate(mockExtensionContext) + + const expectedCommands = [ + 'aws.smus.rootView.refresh', + 'aws.smus.projectView', + 'aws.smus.refreshProject', + 'aws.smus.switchProject', + 'aws.smus.stopSpace', + 'aws.smus.openRemoteConnection', + 'aws.smus.reauthenticate', + ] + + const registeredCommands = registerCommandStub.getCalls().map((call) => call.args[0]) + + for (const command of expectedCommands) { + assert.ok(registeredCommands.includes(command), `Command ${command} should be registered`) + } + }) + + it('should register commands that return disposables', async function () { + await activate(mockExtensionContext) + + for (const call of registerCommandStub.getCalls()) { + const disposable = call.returnValue + assert.ok(disposable && typeof disposable.dispose === 'function') + } + }) + }) + + describe('resource cleanup', function () { + it('should dispose DataZone client on extension deactivation', async function () { + await activate(mockExtensionContext) + + // Find and execute the DataZone dispose subscription + const disposeSubscription = mockExtensionContext.subscriptions.find( + (sub) => sub.dispose && sub.dispose.toString().includes('DataZoneClient.dispose') + ) + + if (disposeSubscription) { + disposeSubscription.dispose() + assert.ok(dataZoneDisposeStub.called) + } + }) + + it('should add tree view to subscriptions for disposal', async function () { + await activate(mockExtensionContext) + + assert.ok(mockExtensionContext.subscriptions.includes(mockTreeView)) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/federatedConnectionStrategy.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/federatedConnectionStrategy.test.ts new file mode 100644 index 00000000000..9aadf68d443 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/federatedConnectionStrategy.test.ts @@ -0,0 +1,185 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { createFederatedConnectionNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/federatedConnectionStrategy' +import { GlueClient, ListEntitiesCommand, DescribeEntityCommand } from '@aws-sdk/client-glue' +import { ConnectionCredentialsProvider } from '../../../../sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider' + +describe('FederatedConnectionStrategy', function () { + let sandbox: sinon.SinonSandbox + let mockGlueClient: sinon.SinonStubbedInstance + let mockCredentialsProvider: ConnectionCredentialsProvider + + const mockConnection = { + connectionId: 'federated-conn-123', + name: 'test-federated-connection', + glueConnectionName: 'test-glue-connection', + } + + beforeEach(function () { + sandbox = sinon.createSandbox() + + mockCredentialsProvider = { + getCredentials: sandbox.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + logger: {} as any, + smusAuthProvider: {} as any, + connectionId: 'test-connection', + projectId: 'test-project', + } as any + + mockGlueClient = sandbox.createStubInstance(GlueClient) + sandbox.stub(GlueClient.prototype, 'send').callsFake(mockGlueClient.send) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('createFederatedConnectionNode', function () { + it('should create connection node with correct properties', async function () { + const node = await createFederatedConnectionNode( + mockConnection as any, + mockCredentialsProvider, + 'us-east-1' + ) + + assert.strictEqual(node.id, 'federated-federated-conn-123') + assert.strictEqual(node.resource, mockConnection) + + const treeItem = await node.getTreeItem() + assert.strictEqual(treeItem.label, 'test-federated-connection') + assert.strictEqual(treeItem.contextValue, 'federatedConnection') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) + }) + + it('should return error when no glue connection name', async function () { + const connectionWithoutGlue = { ...mockConnection, glueConnectionName: undefined } + + const node = await createFederatedConnectionNode( + connectionWithoutGlue as any, + mockCredentialsProvider, + 'us-east-1' + ) + + const children = await node.getChildren!() + assert.strictEqual(children.length, 1) + assert.ok(children[0].id.includes('error')) + }) + + it('should return placeholder when no entities found', async function () { + mockGlueClient.send.resolves({ Entities: [] }) + + const node = await createFederatedConnectionNode( + mockConnection as any, + mockCredentialsProvider, + 'us-east-1' + ) + + const children = await node.getChildren!() + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].resource, '[No data found]') + }) + + it('should group tables under Tables container', async function () { + mockGlueClient.send.resolves({ + Entities: [ + { EntityName: 'table1', Category: 'TABLE', Label: 'Table 1' }, + { EntityName: 'table2', Category: 'TABLE', Label: 'Table 2' }, + ], + }) + + const node = await createFederatedConnectionNode( + mockConnection as any, + mockCredentialsProvider, + 'us-east-1' + ) + + const children = await node.getChildren!() + assert.strictEqual(children.length, 1) + + const tablesContainer = children[0] + assert.ok(tablesContainer.id.includes('tables')) + + const tableChildren = await tablesContainer.getChildren!() + assert.strictEqual(tableChildren.length, 2) + }) + + it('should handle mixed entity types correctly', async function () { + mockGlueClient.send.resolves({ + Entities: [ + { EntityName: 'schema1', Category: 'SCHEMA', Label: 'Schema 1' }, + { EntityName: 'table1', Category: 'TABLE', Label: 'Table 1' }, + ], + }) + + const node = await createFederatedConnectionNode( + mockConnection as any, + mockCredentialsProvider, + 'us-east-1' + ) + + const children = await node.getChildren!() + assert.strictEqual(children.length, 2) // schema + tables container + }) + + it('should handle table columns', async function () { + const mockEntity = { EntityName: 'test-table', Category: 'TABLE' } + + mockGlueClient.send.callsFake((command) => { + if (command instanceof DescribeEntityCommand) { + return Promise.resolve({ + Fields: [ + { FieldName: 'col1', FieldType: 'string', Label: 'Column 1' }, + { FieldName: 'col2', FieldType: 'int', Label: 'Column 2' }, + ], + }) + } + if (command instanceof ListEntitiesCommand) { + return Promise.resolve({ + Entities: [mockEntity], + }) + } + return Promise.resolve({}) + }) + + const node = await createFederatedConnectionNode( + mockConnection as any, + mockCredentialsProvider, + 'us-east-1' + ) + + const children = await node.getChildren!() + const tablesContainer = children[0] + const tableNodes = await tablesContainer.getChildren!() + const tableNode = tableNodes[0] + + const columns = await tableNode.getChildren!() + assert.strictEqual(columns.length, 2) + + const columnTreeItem = await columns[0].getTreeItem() + assert.strictEqual(columnTreeItem.description, 'string') + }) + + it('should handle API errors gracefully', async function () { + mockGlueClient.send.rejects(new Error('API Error')) + + const node = await createFederatedConnectionNode( + mockConnection as any, + mockCredentialsProvider, + 'us-east-1' + ) + + const children = await node.getChildren!() + assert.strictEqual(children.length, 1) + assert.ok(children[0].id.includes('error')) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy.test.ts new file mode 100644 index 00000000000..ebd4780c143 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy.test.ts @@ -0,0 +1,540 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { + LakehouseNode, + createLakehouseConnectionNode, +} from '../../../../sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy' +import { GlueCatalog } from '@amzn/glue-catalog-client' +import { GlueClient } from '../../../../sagemakerunifiedstudio/shared/client/glueClient' +import { ConnectionClientStore } from '../../../../sagemakerunifiedstudio/shared/client/connectionClientStore' +import { NodeType } from '../../../../sagemakerunifiedstudio/explorer/nodes/types' +import { ConnectionCredentialsProvider } from '../../../../sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider' + +describe('LakehouseStrategy', function () { + let sandbox: sinon.SinonSandbox + let mockGlueCatalogClient: sinon.SinonStubbedInstance + let mockGlueClient: sinon.SinonStubbedInstance + + const mockConnection = { + connectionId: 'lakehouse-conn-123', + name: 'test-lakehouse-connection', + type: 'ATHENA', + domainId: 'domain-123', + projectId: 'project-123', + } + + const mockCredentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + getDomainAccountId: async () => '123456789012', + } + + beforeEach(function () { + sandbox = sinon.createSandbox() + + mockGlueCatalogClient = { + getCatalogs: sandbox.stub(), + } as any + + mockGlueClient = { + getDatabases: sandbox.stub(), + getTables: sandbox.stub(), + getTable: sandbox.stub(), + } as any + + // Stub the GlueCatalog constructor to return our mock + sandbox.stub(GlueCatalog.prototype, 'getCatalogs').callsFake(mockGlueCatalogClient.getCatalogs) + sandbox.stub(GlueClient.prototype, 'getDatabases').callsFake(mockGlueClient.getDatabases) + sandbox.stub(GlueClient.prototype, 'getTables').callsFake(mockGlueClient.getTables) + sandbox.stub(GlueClient.prototype, 'getTable').callsFake(mockGlueClient.getTable) + + const mockClientStore = { + getGlueClient: sandbox.stub().returns(mockGlueClient), + getGlueCatalogClient: sandbox.stub().returns(mockGlueCatalogClient), + } + sandbox.stub(ConnectionClientStore, 'getInstance').returns(mockClientStore as any) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('LakehouseNode', function () { + it('should initialize with correct properties', function () { + const nodeData = { + id: 'test-node', + nodeType: NodeType.CONNECTION, + value: { test: 'value' }, + } + + const node = new LakehouseNode(nodeData) + + assert.strictEqual(node.id, 'test-node') + assert.deepStrictEqual(node.resource, { test: 'value' }) + }) + + it('should return empty array for leaf nodes', async function () { + const nodeData = { + id: 'leaf-node', + nodeType: NodeType.REDSHIFT_COLUMN, + value: {}, + } + + const node = new LakehouseNode(nodeData) + const children = await node.getChildren() + + assert.strictEqual(children.length, 0) + }) + + it('should return error node when children provider fails', async function () { + const nodeData = { + id: 'error-node', + nodeType: NodeType.CONNECTION, + value: {}, + } + + const failingProvider = async () => { + throw new Error('Provider failed') + } + + const node = new LakehouseNode(nodeData, failingProvider) + const children = await node.getChildren() + + assert.strictEqual(children.length, 1) + assert.ok(children[0].id.startsWith('error-node-error-getChildren-')) + }) + + it('should create correct tree item for column node', async function () { + const nodeData = { + id: 'column-node', + nodeType: NodeType.REDSHIFT_COLUMN, + value: { name: 'test_column', type: 'varchar' }, + } + + const node = new LakehouseNode(nodeData) + const treeItem = await node.getTreeItem() + + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.strictEqual(treeItem.description, 'varchar') + }) + + it('should cache children after first load', async function () { + const provider = sandbox + .stub() + .resolves([new LakehouseNode({ id: 'child', nodeType: NodeType.GLUE_DATABASE })]) + const node = new LakehouseNode({ id: 'parent', nodeType: NodeType.CONNECTION }, provider) + + await node.getChildren() + await node.getChildren() + + assert.ok(provider.calledOnce) + }) + }) + + describe('createLakehouseConnectionNode', function () { + it('should create connection node with correct structure', function () { + const node = createLakehouseConnectionNode( + mockConnection as any, + mockCredentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + + assert.strictEqual(node.id, 'lakehouse-conn-123') + assert.strictEqual(node.data.nodeType, NodeType.CONNECTION) + assert.strictEqual(node.data.path?.connection, 'test-lakehouse-connection') + }) + + it('should create AWS Data Catalog node for default connections', async function () { + const defaultConnection = { + ...mockConnection, + name: 'project.default_lakehouse', + } + + mockGlueCatalogClient.getCatalogs.resolves({ catalogs: [], nextToken: undefined }) + mockGlueClient.getDatabases.resolves({ + databases: [{ Name: 'default-db' }], + nextToken: undefined, + }) + + const node = createLakehouseConnectionNode( + defaultConnection as any, + mockCredentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + const children = await node.getChildren() + + const awsDataCatalogNode = children.find((child) => child.id.includes('AwsDataCatalog')) as LakehouseNode + assert.ok(awsDataCatalogNode) + assert.strictEqual(awsDataCatalogNode.data.nodeType, NodeType.GLUE_CATALOG) + }) + + it('should not create AWS Data Catalog node for non-default connections', async function () { + mockGlueCatalogClient.getCatalogs.resolves({ catalogs: [], nextToken: undefined }) + + const node = createLakehouseConnectionNode( + mockConnection as any, + mockCredentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + const children = await node.getChildren() + + const awsDataCatalogNode = children.find((child) => child.id.includes('AwsDataCatalog')) + assert.strictEqual(awsDataCatalogNode, undefined) + }) + + it('should handle errors gracefully', async function () { + mockGlueCatalogClient.getCatalogs.rejects(new Error('Catalog error')) + mockGlueClient.getDatabases.rejects(new Error('Database error')) + + const node = createLakehouseConnectionNode( + mockConnection as any, + mockCredentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + const children = await node.getChildren() + + assert.ok(children.length > 0) + assert.ok(children.some((child) => child.id.startsWith('lakehouse-conn-123-error-'))) + }) + + it('should create placeholder when no catalogs found', async function () { + // Mock getCatalogs to return empty array in the correct format + mockGlueCatalogClient.getCatalogs.resolves({ catalogs: [], nextToken: undefined }) + + const node = createLakehouseConnectionNode( + mockConnection as any, + mockCredentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + const children = await node.getChildren() + + // Check if placeholder exists - it should be a TreeItem with label '[No data found]' + const hasPlaceholder = children.some((child) => { + const treeItem = (child as any).data?.value + return treeItem === '[No data found]' || child.id.includes('placeholder') + }) + assert.ok(hasPlaceholder, 'Should have placeholder node when no catalogs found') + }) + }) + + describe('Catalog nodes', function () { + it('should create regular catalog nodes and load databases', async function () { + const regularCatalog = { + CatalogId: 'regular-catalog', + Name: 'regular-catalog', + CatalogType: 'HIVE', + } + + mockGlueCatalogClient.getCatalogs.resolves({ + catalogs: [regularCatalog], + nextToken: undefined, + }) + mockGlueClient.getDatabases.resolves({ + databases: [{ Name: 'test-db' }], + nextToken: undefined, + }) + + const node = createLakehouseConnectionNode( + mockConnection as any, + mockCredentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + const children = await node.getChildren() + + const catalogNode = children.find((child) => child.id === 'regular-catalog') as LakehouseNode + assert.ok(catalogNode) + assert.ok(mockGlueCatalogClient.getCatalogs.called) + + const catalogChildren = await catalogNode.getChildren() + assert.strictEqual(catalogChildren.length, 1) + assert.strictEqual((catalogChildren[0] as LakehouseNode).data.nodeType, NodeType.GLUE_DATABASE) + }) + + it('should handle catalog database pagination', async function () { + const catalogNode = new LakehouseNode( + { + id: 'catalog-node', + nodeType: NodeType.GLUE_CATALOG, + path: { catalog: 'test-catalog' }, + }, + async () => { + const allDatabases = [] + let nextToken: string | undefined + do { + const { databases, nextToken: token } = await mockGlueClient.getDatabases( + 'test-catalog', + undefined, + undefined, + nextToken + ) + allDatabases.push(...databases) + nextToken = token + } while (nextToken) + return allDatabases.map( + (db) => new LakehouseNode({ id: db.Name || '', nodeType: NodeType.GLUE_DATABASE }) + ) + } + ) + + mockGlueClient.getDatabases + .onFirstCall() + .resolves({ databases: [{ Name: 'db1' }], nextToken: 'token1' }) + .onSecondCall() + .resolves({ databases: [{ Name: 'db2' }], nextToken: undefined }) + + const children = await catalogNode.getChildren() + + assert.strictEqual(children.length, 2) + assert.ok(mockGlueClient.getDatabases.calledTwice) + }) + + it('should treat RedLake catalogs as parent catalogs', async function () { + const redLakeCatalog = { + CatalogId: 'redlake-catalog', + Name: 'redlake-catalog', + FederatedCatalog: { + ConnectionName: 'aws:redshift', + }, + } + + mockGlueCatalogClient.getCatalogs.resolves({ + catalogs: [redLakeCatalog], + nextToken: undefined, + }) + + const node = createLakehouseConnectionNode( + mockConnection as any, + mockCredentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + const children = await node.getChildren() + + const catalogNode = children.find((child) => child.id === 'redlake-catalog') as LakehouseNode + assert.ok(catalogNode) + + const catalogChildren = await catalogNode.getChildren() + assert.strictEqual(catalogChildren.length, 1) + assert.ok(catalogChildren[0].resource === '[No data found]') + }) + + it('should treat S3Tables catalogs as parent catalogs', async function () { + const s3TablesCatalog = { + CatalogId: 's3tables-catalog', + Name: 's3tables-catalog', + FederatedCatalog: { + ConnectionName: 'aws:s3tables', + }, + } + + mockGlueCatalogClient.getCatalogs.resolves({ + catalogs: [s3TablesCatalog], + nextToken: undefined, + }) + + const node = createLakehouseConnectionNode( + mockConnection as any, + mockCredentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + const children = await node.getChildren() + + const catalogNode = children.find((child) => child.id === 's3tables-catalog') as LakehouseNode + assert.ok(catalogNode) + + const catalogChildren = await catalogNode.getChildren() + assert.strictEqual(catalogChildren.length, 1) + assert.ok(catalogChildren[0].resource === '[No data found]') + }) + }) + + describe('Database nodes', function () { + it('should handle table pagination', async function () { + const databaseNode = new LakehouseNode( + { + id: 'database-node', + nodeType: NodeType.GLUE_DATABASE, + path: { catalog: 'test-catalog', database: 'test-db' }, + }, + async () => { + const allTables = [] + let nextToken: string | undefined + do { + const { tables, nextToken: token } = await mockGlueClient.getTables( + 'test-db', + 'test-catalog', + undefined, + nextToken + ) + allTables.push(...tables) + nextToken = token + } while (nextToken) + return allTables.map( + (table) => new LakehouseNode({ id: table.Name || '', nodeType: NodeType.GLUE_TABLE }) + ) + } + ) + + mockGlueClient.getTables + .onFirstCall() + .resolves({ tables: [{ Name: 'table1' }], nextToken: 'token1' }) + .onSecondCall() + .resolves({ tables: [{ Name: 'table2' }], nextToken: undefined }) + + const children = await databaseNode.getChildren() + + assert.strictEqual(children.length, 2) + assert.ok(mockGlueClient.getTables.calledTwice) + }) + + it('should handle AWS Data Catalog database queries', async function () { + const databaseNode = new LakehouseNode( + { + id: 'database-node', + nodeType: NodeType.GLUE_DATABASE, + path: { catalog: 'aws-data-catalog', database: 'test-db' }, + }, + async () => { + const catalogId = undefined + const { tables } = await mockGlueClient.getTables('test-db', catalogId) + return tables.map( + (table) => new LakehouseNode({ id: table.Name || '', nodeType: NodeType.GLUE_TABLE }) + ) + } + ) + + mockGlueClient.getTables.resolves({ tables: [{ Name: 'aws-table' }], nextToken: undefined }) + + const children = await databaseNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.ok(mockGlueClient.getTables.calledWith('test-db', undefined)) + }) + }) + + describe('Table nodes', function () { + it('should create table node and load columns', async function () { + const tableNode = new LakehouseNode( + { + id: 'table-node', + nodeType: NodeType.GLUE_TABLE, + path: { database: 'test-db', table: 'test-table' }, + }, + async () => { + const tableDetails = await mockGlueClient.getTable('test-db', 'test-table') + const columns = tableDetails?.StorageDescriptor?.Columns || [] + const partitions = tableDetails?.PartitionKeys || [] + return [...columns, ...partitions].map( + (col) => + new LakehouseNode({ + id: `column-${col.Name}`, + nodeType: NodeType.REDSHIFT_COLUMN, + value: { name: col.Name, type: col.Type }, + }) + ) + } + ) + + mockGlueClient.getTable.resolves({ + StorageDescriptor: { + Columns: [{ Name: 'col1', Type: 'string' }], + }, + PartitionKeys: [{ Name: 'partition_col', Type: 'date' }], + Name: undefined, + }) + + const children = await tableNode.getChildren() + + assert.strictEqual(children.length, 2) + assert.ok(mockGlueClient.getTable.calledWith('test-db', 'test-table')) + }) + + it('should handle table with no columns', async function () { + const tableNode = new LakehouseNode( + { + id: 'empty-table-node', + nodeType: NodeType.GLUE_TABLE, + path: { database: 'test-db', table: 'empty-table' }, + }, + async () => { + const tableDetails = await mockGlueClient.getTable('test-db', 'empty-table') + const columns = tableDetails?.StorageDescriptor?.Columns || [] + const partitions = tableDetails?.PartitionKeys || [] + return [...columns, ...partitions].map( + (col) => + new LakehouseNode({ + id: `column-${col.Name}`, + nodeType: NodeType.REDSHIFT_COLUMN, + value: { name: col.Name, type: col.Type }, + }) + ) + } + ) + + mockGlueClient.getTable.resolves({ + StorageDescriptor: { Columns: [] }, + PartitionKeys: [], + Name: undefined, + }) + + const children = await tableNode.getChildren() + + assert.strictEqual(children.length, 0) + }) + + it('should handle table getTable errors gracefully', async function () { + const tableNode = new LakehouseNode( + { + id: 'error-table-node', + nodeType: NodeType.GLUE_TABLE, + path: { database: 'test-db', table: 'error-table' }, + }, + async () => { + try { + await mockGlueClient.getTable('test-db', 'error-table') + return [] + } catch (err) { + return [] + } + } + ) + + mockGlueClient.getTable.rejects(new Error('Table not found')) + + const children = await tableNode.getChildren() + + assert.strictEqual(children.length, 0) + }) + }) + + describe('Column nodes', function () { + it('should create column node with correct properties', function () { + const parentNode = new LakehouseNode({ + id: 'parent-table', + nodeType: NodeType.GLUE_TABLE, + path: { database: 'test-db', table: 'test-table' }, + }) + + const columnNode = new LakehouseNode({ + id: 'parent-table/test-column', + nodeType: NodeType.REDSHIFT_COLUMN, + value: { name: 'test-column', type: 'varchar' }, + path: { database: 'test-db', table: 'test-table', column: 'test-column' }, + parent: parentNode, + }) + + assert.strictEqual(columnNode.id, 'parent-table/test-column') + assert.strictEqual(columnNode.resource.name, 'test-column') + assert.strictEqual(columnNode.resource.type, 'varchar') + assert.strictEqual(columnNode.getParent(), parentNode) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/redshiftStrategy.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/redshiftStrategy.test.ts new file mode 100644 index 00000000000..50b5e36e251 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/redshiftStrategy.test.ts @@ -0,0 +1,359 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { + RedshiftNode, + createRedshiftConnectionNode, +} from '../../../../sagemakerunifiedstudio/explorer/nodes/redshiftStrategy' +import { SQLWorkbenchClient } from '../../../../sagemakerunifiedstudio/shared/client/sqlWorkbenchClient' +import * as sqlWorkbenchClient from '../../../../sagemakerunifiedstudio/shared/client/sqlWorkbenchClient' +import { ConnectionClientStore } from '../../../../sagemakerunifiedstudio/shared/client/connectionClientStore' +import { NodeType } from '../../../../sagemakerunifiedstudio/explorer/nodes/types' +import { ConnectionCredentialsProvider } from '../../../../sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider' + +describe('redshiftStrategy', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('RedshiftNode', function () { + describe('constructor', function () { + it('should create node with correct properties', function () { + const nodeData = { + id: 'test-id', + nodeType: NodeType.REDSHIFT_CLUSTER, + value: { clusterName: 'test-cluster' }, + } + + const node = new RedshiftNode(nodeData) + + assert.strictEqual(node.id, 'test-id') + assert.strictEqual(node.data.nodeType, NodeType.REDSHIFT_CLUSTER) + assert.deepStrictEqual(node.resource, { clusterName: 'test-cluster' }) + }) + }) + + describe('getChildren', function () { + it('should return cached children if available', async function () { + const nodeData = { + id: 'test-id', + nodeType: NodeType.REDSHIFT_CLUSTER, + } + + const node = new RedshiftNode(nodeData) + // Simulate cached children + ;(node as any).childrenNodes = [{ id: 'cached-child' }] + + const children = await node.getChildren() + assert.strictEqual(children.length, 1) + assert.strictEqual((children[0] as any).id, 'cached-child') + }) + + it('should return empty array for leaf nodes', async function () { + const nodeData = { + id: 'test-id', + nodeType: NodeType.REDSHIFT_COLUMN, + } + + const node = new RedshiftNode(nodeData) + const children = await node.getChildren() + assert.strictEqual(children.length, 0) + }) + }) + + describe('getTreeItem', function () { + it('should return correct tree item for regular nodes', async function () { + const nodeData = { + id: 'test-cluster', + nodeType: NodeType.REDSHIFT_CLUSTER, + value: { clusterName: 'test-cluster' }, + } + + const node = new RedshiftNode(nodeData) + const treeItem = await node.getTreeItem() + + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) + assert.strictEqual(treeItem.contextValue, NodeType.REDSHIFT_CLUSTER) + }) + + it('should return column tree item for column nodes', async function () { + const nodeData = { + id: 'test-column', + nodeType: NodeType.REDSHIFT_COLUMN, + value: { type: 'VARCHAR(255)' }, + } + + const node = new RedshiftNode(nodeData) + const treeItem = await node.getTreeItem() + + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.strictEqual(treeItem.description, 'VARCHAR(255)') + }) + + it('should return leaf tree item for leaf nodes', async function () { + const nodeData = { + id: 'test-column', + nodeType: NodeType.REDSHIFT_COLUMN, + } + + const node = new RedshiftNode(nodeData) + const treeItem = await node.getTreeItem() + + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + }) + }) + + describe('getParent', function () { + it('should return parent node', function () { + const parentData = { id: 'parent', nodeType: NodeType.REDSHIFT_CLUSTER } + const parent = new RedshiftNode(parentData) + + const nodeData = { + id: 'child', + nodeType: NodeType.REDSHIFT_DATABASE, + parent: parent, + } + + const node = new RedshiftNode(nodeData) + assert.strictEqual(node.getParent(), parent) + }) + }) + }) + + describe('createRedshiftConnectionNode', function () { + let mockSQLClient: sinon.SinonStubbedInstance + + beforeEach(function () { + mockSQLClient = { + executeQuery: sandbox.stub(), + getResources: sandbox.stub(), + } as any + + sandbox.stub(SQLWorkbenchClient, 'createWithCredentials').returns(mockSQLClient as any) + sandbox.stub(sqlWorkbenchClient, 'createRedshiftConnectionConfig').resolves({ + id: 'test-connection-id', + type: '4', + databaseType: 'REDSHIFT', + connectableResourceIdentifier: 'test-cluster', + connectableResourceType: 'CLUSTER', + database: 'test-db', + }) + + const mockClientStore = { + getSQLWorkbenchClient: sandbox.stub().returns(mockSQLClient), + } + sandbox.stub(ConnectionClientStore, 'getInstance').returns(mockClientStore as any) + }) + + it.skip('should create Redshift connection node with JDBC URL', async function () { + const connection = { + connectionId: 'conn-123', + name: 'Test Redshift Connection', + type: 'RedshiftConnection', + props: { + jdbcConnection: { + jdbcUrl: 'jdbc:redshift://test-cluster.123456789012.us-east-1.redshift.amazonaws.com:5439/dev', + dbname: 'test-db', + }, + redshiftProperties: {}, + }, + location: { + awsAccountId: '', + awsRegion: 'us-east-1', + }, + } + + const credentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + } + + mockSQLClient.executeQuery.resolves('query-id') + mockSQLClient.getResources.resolves({ + resources: [ + { + displayName: 'test-db', + type: 'DATABASE', + identifier: '', + childObjectTypes: [], + }, + ], + }) + + const node = createRedshiftConnectionNode( + connection as any, + credentialsProvider as ConnectionCredentialsProvider + ) + + assert.strictEqual(node.data.nodeType, NodeType.CONNECTION) + assert.strictEqual(node.data.value.connection.name, 'Test Redshift Connection') + + // Test children provider - now creates database nodes directly + const children = await node.getChildren() + assert.strictEqual(children.length, 1) + assert.strictEqual((children[0] as RedshiftNode).data.nodeType, NodeType.REDSHIFT_DATABASE) + }) + + it.skip('should create connection node with host from jdbcConnection', async function () { + const connection = { + connectionId: 'conn-123', + name: 'Test Connection', + type: 'RedshiftConnection', + props: { + jdbcConnection: { + host: 'test-host.redshift.amazonaws.com', + dbname: 'test-db', + }, + redshiftProperties: {}, + }, + location: { + awsAccountId: '', + awsRegion: 'us-east-1', + }, + } + + const credentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + } + + mockSQLClient.executeQuery.resolves('query-id') + mockSQLClient.getResources.resolves({ resources: [] }) + + const node = createRedshiftConnectionNode( + connection as any, + credentialsProvider as ConnectionCredentialsProvider + ) + const children = await node.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual((children[0] as RedshiftNode).data.nodeType, NodeType.REDSHIFT_DATABASE) + }) + + it('should return placeholder when connection params are missing', async function () { + const connection = { + connectionId: 'conn-123', + name: 'Test Connection', + type: 'RedshiftConnection', + props: { + jdbcConnection: {}, + redshiftProperties: {}, + }, + location: {}, + } + + const credentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + getDomainAccountId: async () => '123456789012', + } + + const node = createRedshiftConnectionNode( + connection as any, + credentialsProvider as ConnectionCredentialsProvider + ) + const children = await node.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].resource, '[No data found]') + }) + + it.skip('should handle workgroup name in host', async function () { + const connection = { + connectionId: 'conn-123', + name: 'Test Connection', + type: 'RedshiftConnection', + props: { + jdbcConnection: { + host: 'test-host.redshift-serverless.amazonaws.com', + dbname: 'test-db', + }, + redshiftProperties: { + storage: { + workgroupName: 'test-workgroup', + }, + }, + }, + location: { + awsAccountId: '', + awsRegion: 'us-east-1', + }, + } + + const credentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + } + + mockSQLClient.executeQuery.resolves('query-id') + mockSQLClient.getResources.resolves({ resources: [] }) + + const node = createRedshiftConnectionNode( + connection as any, + credentialsProvider as ConnectionCredentialsProvider + ) + const children = await node.getChildren() + + assert.strictEqual(children.length, 1) + }) + + it.skip('should handle connection errors gracefully', async function () { + const connection = { + connectionId: 'conn-123', + name: 'Test Connection', + type: 'RedshiftConnection', + props: { + jdbcConnection: { + host: 'test-host.redshift.amazonaws.com', + dbname: 'test-db', + }, + }, + location: { + awsAccountId: '', + awsRegion: 'us-east-1', + }, + } + + const credentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + } + + // Make createRedshiftConnectionConfig throw an error + ;(sqlWorkbenchClient.createRedshiftConnectionConfig as sinon.SinonStub).rejects( + new Error('Connection config failed') + ) + + const node = createRedshiftConnectionNode( + connection as any, + credentialsProvider as ConnectionCredentialsProvider + ) + + // The error should be handled gracefully and return an error node + const children = await node.getChildren() + assert.strictEqual(children.length, 1) + assert.strictEqual((children[0] as any).id.includes('error'), true) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/s3Strategy.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/s3Strategy.test.ts new file mode 100644 index 00000000000..f6838ab483e --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/s3Strategy.test.ts @@ -0,0 +1,253 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { S3Node, createS3ConnectionNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/s3Strategy' +import { S3Client } from '../../../../sagemakerunifiedstudio/shared/client/s3Client' +import { ConnectionClientStore } from '../../../../sagemakerunifiedstudio/shared/client/connectionClientStore' +import { NodeType, ConnectionType } from '../../../../sagemakerunifiedstudio/explorer/nodes/types' +import { ConnectionCredentialsProvider } from '../../../../sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider' +import { createMockS3Connection, createMockCredentialsProvider } from '../../testUtils' + +describe('s3Strategy', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('S3Node', function () { + describe('constructor', function () { + it('should create node with correct properties', function () { + const node = new S3Node({ + id: 'test-id', + nodeType: NodeType.S3_BUCKET, + connectionType: ConnectionType.S3, + value: { bucket: 'test-bucket' }, + path: { bucket: 'test-bucket' }, + }) + + assert.strictEqual(node.id, 'test-id') + assert.strictEqual(node.data.nodeType, NodeType.S3_BUCKET) + assert.strictEqual(node.data.connectionType, ConnectionType.S3) + }) + }) + + describe('getChildren', function () { + it('should return empty array for leaf nodes', async function () { + const node = new S3Node({ + id: 'test-id', + nodeType: NodeType.S3_FILE, + connectionType: ConnectionType.S3, + }) + + const children = await node.getChildren() + assert.strictEqual(children.length, 0) + }) + + it('should handle children provider errors', async function () { + const errorProvider = async () => { + throw new Error('Provider error') + } + + const node = new S3Node( + { + id: 'test-id', + nodeType: NodeType.S3_BUCKET, + connectionType: ConnectionType.S3, + }, + errorProvider + ) + + const children = await node.getChildren() + assert.strictEqual(children.length, 1) + assert.ok(children[0].id.startsWith('test-id-error-getChildren-')) + }) + }) + + describe('getTreeItem', function () { + it('should return correct tree item for non-leaf node', async function () { + const node = new S3Node({ + id: 'test-id', + nodeType: NodeType.S3_BUCKET, + connectionType: ConnectionType.S3, + }) + + const treeItem = await node.getTreeItem() + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) + assert.strictEqual(treeItem.contextValue, NodeType.S3_BUCKET) + }) + + it('should return correct tree item for leaf node', async function () { + const node = new S3Node({ + id: 'test-id', + nodeType: NodeType.S3_FILE, + connectionType: ConnectionType.S3, + }) + + const treeItem = await node.getTreeItem() + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + }) + }) + }) + + describe('createS3ConnectionNode', function () { + let mockS3Client: sinon.SinonStubbedInstance + + beforeEach(function () { + mockS3Client = { + listPaths: sandbox.stub(), + } as any + + sandbox.stub(S3Client.prototype, 'constructor' as any) + sandbox.stub(S3Client.prototype, 'listPaths').callsFake(mockS3Client.listPaths) + + const mockClientStore = { + getS3Client: sandbox.stub().returns(mockS3Client), + } + sandbox.stub(ConnectionClientStore, 'getInstance').returns(mockClientStore as any) + }) + + it('should create S3 connection node successfully for non-default connection', function () { + const connection = { + connectionId: 'conn-123', + name: 'Test S3 Connection', + type: 'S3Connection', + props: { + s3Properties: { + s3Uri: 's3://test-bucket/prefix/', + }, + }, + } + + const credentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + } + + const node = createS3ConnectionNode( + connection as any, + credentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + + assert.strictEqual(node.data.nodeType, NodeType.CONNECTION) + assert.strictEqual(node.data.connectionType, ConnectionType.S3) + }) + + it('should create S3 connection node for default connection with full path', function () { + const connection = createMockS3Connection() + const credentialsProvider = createMockCredentialsProvider() + + const node = createS3ConnectionNode( + connection as any, + credentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + + assert.strictEqual(node.data.nodeType, NodeType.CONNECTION) + assert.strictEqual(node.data.connectionType, ConnectionType.S3) + }) + + it('should return error node when no S3 URI found', function () { + const connection = { + connectionId: 'conn-123', + name: 'Test S3 Connection', + type: 'S3Connection', + props: {}, + } + + const credentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + } + + const node = createS3ConnectionNode( + connection as any, + credentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + + assert.ok(node.id.startsWith('conn-123-error-connection-')) + }) + + it('should handle bucket listing for non-default connection', async function () { + const connection = { + connectionId: 'conn-123', + name: 'Test S3 Connection', + type: 'S3Connection', + props: { + s3Properties: { + s3Uri: 's3://test-bucket/', + }, + }, + } + + const credentialsProvider = createMockCredentialsProvider() + + mockS3Client.listPaths.resolves({ + paths: [ + { + bucket: 'test-bucket', + prefix: 'file.txt', + displayName: 'file.txt', + isFolder: false, + }, + ], + nextToken: undefined, + }) + + const node = createS3ConnectionNode( + connection as any, + credentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + const children = await node.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual((children[0] as S3Node).data.nodeType, NodeType.S3_BUCKET) + }) + + it('should handle bucket listing for default connection with full path display', async function () { + const connection = createMockS3Connection() + const credentialsProvider = createMockCredentialsProvider() + + mockS3Client.listPaths.resolves({ + paths: [ + { + bucket: 'test-bucket', + prefix: 'domain/project/dev/', + displayName: 'dev', + isFolder: true, + }, + ], + nextToken: undefined, + }) + + const node = createS3ConnectionNode( + connection as any, + credentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + const children = await node.getChildren() + + assert.strictEqual(children.length, 1) + const bucketNode = children[0] as S3Node + assert.strictEqual(bucketNode.data.nodeType, NodeType.S3_BUCKET) + // For default connection, should show full path + assert.strictEqual(bucketNode.data.path?.label, 'test-bucket/domain/project/') + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioAuthInfoNode.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioAuthInfoNode.test.ts new file mode 100644 index 00000000000..52d1d045403 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioAuthInfoNode.test.ts @@ -0,0 +1,426 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import * as vscode from 'vscode' +import { SageMakerUnifiedStudioAuthInfoNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioAuthInfoNode' +import { SmusAuthenticationProvider } from '../../../../sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider' +import { SmusConnection, SmusSsoConnection } from '../../../../sagemakerunifiedstudio/auth/model' + +describe('SageMakerUnifiedStudioAuthInfoNode', function () { + let authInfoNode: SageMakerUnifiedStudioAuthInfoNode + let mockAuthProvider: any + let mockConnection: SmusSsoConnection + let currentActiveConnection: SmusConnection | undefined + + beforeEach(function () { + mockConnection = { + id: 'test-connection-id', + type: 'sso', + startUrl: 'https://identitycenter.amazonaws.com/ssoins-testInstanceId', + ssoRegion: 'us-east-2', + scopes: ['datazone:domain:access'], + label: 'Test SMUS Connection', + domainUrl: 'https://dzd_domainId.sagemaker.us-east-2.on.aws', + domainId: 'dzd_domainId', + // Mock the required methods from SsoConnection + getToken: sinon.stub().resolves(), + getRegistration: sinon.stub().resolves(), + } as any + + // Initialize the current active connection + currentActiveConnection = mockConnection + + // Create mock auth provider with getter for activeConnection + mockAuthProvider = { + isConnected: sinon.stub().returns(true), + isConnectionValid: sinon.stub().returns(true), + onDidChange: sinon.stub().callsFake((listener: () => void) => ({ dispose: sinon.stub() })), + onDidChangeActiveConnection: sinon.stub().callsFake((listener: () => void) => ({ dispose: sinon.stub() })), + getDomainId: sinon.stub().callsFake(() => { + return currentActiveConnection?.domainId + }), + getDomainRegion: sinon.stub().callsFake(() => { + if (currentActiveConnection?.type === 'sso') { + return (currentActiveConnection as any).ssoRegion + } else if (currentActiveConnection?.type === 'iam') { + return (currentActiveConnection as any).region + } + return undefined + }), + getSessionName: sinon.stub().resolves(undefined), + getRoleArn: sinon.stub().resolves(undefined), + get activeConnection() { + return currentActiveConnection + }, + set activeConnection(value: SmusConnection | undefined) { + currentActiveConnection = value + }, + } + + // Stub getContext to return false for IAM mode by default (SSO connections) + sinon.stub(require('../../../../shared/vscode/setContext'), 'getContext').returns(false) + + // Stub SmusAuthenticationProvider.fromContext + sinon.stub(SmusAuthenticationProvider, 'fromContext').returns(mockAuthProvider as any) + + authInfoNode = new SageMakerUnifiedStudioAuthInfoNode() + }) + + afterEach(function () { + sinon.restore() + }) + + describe('constructor', function () { + it('should initialize with correct properties', function () { + assert.strictEqual(authInfoNode.id, 'smusAuthInfoNode') + assert.strictEqual(authInfoNode.resource, authInfoNode) + }) + + it('should register for auth provider changes', function () { + assert.ok(mockAuthProvider.onDidChange.called) + }) + + it('should have onDidChangeTreeItem event', function () { + assert.ok(typeof authInfoNode.onDidChangeTreeItem === 'function') + }) + }) + + describe('getTreeItem', function () { + describe('when connected and valid', function () { + beforeEach(function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(true) + mockAuthProvider.activeConnection = mockConnection + }) + + it('should return connected tree item', async function () { + const treeItem = await authInfoNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Domain: dzd_domainId') + assert.strictEqual(treeItem.description, 'us-east-2') + assert.strictEqual(treeItem.contextValue, 'smusAuthInfo') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + + // Check icon + assert.ok(treeItem.iconPath instanceof vscode.ThemeIcon) + assert.strictEqual((treeItem.iconPath as vscode.ThemeIcon).id, 'key') + + // Check tooltip + const tooltip = treeItem.tooltip as string + assert.ok(tooltip?.includes('Connected to SageMaker Unified Studio')) + assert.ok(tooltip?.includes('dzd_domainId')) + assert.ok(tooltip?.includes('us-east-2')) + assert.ok(tooltip?.includes('Status: Connected')) + + // Should not have command when valid + assert.strictEqual(treeItem.command, undefined) + }) + }) + + describe('when connected but expired', function () { + beforeEach(function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(false) + mockAuthProvider.activeConnection = mockConnection + }) + + it('should return expired tree item with reauthenticate command', async function () { + const treeItem = await authInfoNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Domain: dzd_domainId (Expired) - Click to reauthenticate') + assert.strictEqual(treeItem.description, 'us-east-2') + assert.strictEqual(treeItem.contextValue, 'smusAuthInfo') + + // Check icon + assert.ok(treeItem.iconPath instanceof vscode.ThemeIcon) + assert.strictEqual((treeItem.iconPath as vscode.ThemeIcon).id, 'warning') + + // Check tooltip + const tooltip = treeItem.tooltip as string + assert.ok(tooltip?.includes('Connection to SageMaker Unified Studio has expired')) + assert.ok(tooltip?.includes('Status: Expired - Click to reauthenticate')) + + // Should have reauthenticate command + assert.ok(treeItem.command) + assert.strictEqual(treeItem.command.command, 'aws.smus.reauthenticate') + assert.strictEqual(treeItem.command.title, 'Reauthenticate') + assert.deepStrictEqual(treeItem.command.arguments, [mockConnection]) + }) + }) + + describe('when not connected', function () { + beforeEach(function () { + mockAuthProvider.isConnected.returns(false) + mockAuthProvider.isConnectionValid.returns(false) + mockAuthProvider.activeConnection = undefined + }) + + it('should return not connected tree item', async function () { + const treeItem = await authInfoNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Not Connected') + assert.strictEqual(treeItem.description, undefined) + assert.strictEqual(treeItem.contextValue, 'smusAuthInfo') + + // Check icon + assert.ok(treeItem.iconPath instanceof vscode.ThemeIcon) + assert.strictEqual((treeItem.iconPath as vscode.ThemeIcon).id, 'circle-slash') + + // Check tooltip + const tooltip = treeItem.tooltip as string + assert.ok(tooltip?.includes('Not connected to SageMaker Unified Studio')) + assert.ok(tooltip?.includes('Please sign in to access your projects')) + + // Should not have command when not connected + assert.strictEqual(treeItem.command, undefined) + }) + }) + + describe('with missing connection details', function () { + beforeEach(function () { + const incompleteConnection = { + ...mockConnection, + domainId: undefined, + ssoRegion: undefined, + } as any + + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(true) + mockAuthProvider.activeConnection = incompleteConnection + }) + + it('should handle missing domain ID and region gracefully', async function () { + const treeItem = await authInfoNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Domain: Unknown') + assert.strictEqual(treeItem.description, 'Unknown') + + const tooltip = treeItem.tooltip as string + assert.ok(tooltip?.includes('Domain ID: Unknown')) + assert.ok(tooltip?.includes('Region: Unknown')) + }) + }) + }) + + describe('getParent', function () { + it('should return undefined', function () { + assert.strictEqual(authInfoNode.getParent(), undefined) + }) + }) + + describe('event handling', function () { + it('should fire onDidChangeTreeItem when auth provider changes', function () { + const eventSpy = sinon.spy() + authInfoNode.onDidChangeTreeItem(eventSpy) + + // Simulate auth provider change + const onDidChangeCallback = mockAuthProvider.onDidChange.firstCall.args[0] + onDidChangeCallback() + + assert.ok(eventSpy.called) + }) + + it('should dispose event listener properly', function () { + const disposeSpy = sinon.spy() + mockAuthProvider.onDidChange.returns({ dispose: disposeSpy }) + + // Create new node to trigger event listener registration + new SageMakerUnifiedStudioAuthInfoNode() + + // The dispose should be available for cleanup + assert.ok(mockAuthProvider.onDidChange.called) + }) + }) + + describe('theme icon colors', function () { + it('should use green color for connected state', async function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(true) + + const treeItem = await authInfoNode.getTreeItem() + const icon = treeItem.iconPath as vscode.ThemeIcon + + assert.ok(icon.color instanceof vscode.ThemeColor) + assert.strictEqual((icon.color as any).id, 'charts.green') + }) + + it('should use yellow color for expired state', async function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(false) + + const treeItem = await authInfoNode.getTreeItem() + const icon = treeItem.iconPath as vscode.ThemeIcon + + assert.ok(icon.color instanceof vscode.ThemeColor) + assert.strictEqual((icon.color as any).id, 'charts.yellow') + }) + + it('should use red color for not connected state', async function () { + mockAuthProvider.isConnected.returns(false) + + const treeItem = await authInfoNode.getTreeItem() + const icon = treeItem.iconPath as vscode.ThemeIcon + + assert.ok(icon.color instanceof vscode.ThemeColor) + assert.strictEqual((icon.color as any).id, 'charts.red') + }) + }) + + describe('tooltip content', function () { + it('should include all relevant information for connected state', async function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(true) + + const treeItem = await authInfoNode.getTreeItem() + const tooltip = treeItem.tooltip as string + + assert.ok(tooltip.includes('Connected to SageMaker Unified Studio')) + assert.ok(tooltip.includes(`Domain ID: ${mockConnection.domainId}`)) + assert.ok(tooltip.includes(`Region: ${mockConnection.ssoRegion}`)) + assert.ok(tooltip.includes('Status: Connected')) + }) + + it('should include expiration information for expired state', async function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(false) + + const treeItem = await authInfoNode.getTreeItem() + const tooltip = treeItem.tooltip as string + + assert.ok(tooltip.includes('Connection to SageMaker Unified Studio has expired')) + assert.ok(tooltip.includes('Status: Expired - Click to reauthenticate')) + }) + + it('should include sign-in prompt for not connected state', async function () { + mockAuthProvider.isConnected.returns(false) + + const treeItem = await authInfoNode.getTreeItem() + const tooltip = treeItem.tooltip as string + + assert.ok(tooltip.includes('Not connected to SageMaker Unified Studio')) + assert.ok(tooltip.includes('Please sign in to access your projects')) + }) + }) + + describe('IAM connections in IAM mode', function () { + let mockIamConnection: any + + beforeEach(function () { + mockIamConnection = { + id: 'profile:test-profile', + type: 'iam', + label: 'Test IAM Profile', + profileName: 'test-profile', + region: 'us-west-2', + domainUrl: 'https://dzd_domainId.sagemaker.us-west-2.on.aws', + domainId: 'dzd_domainId', + getCredentials: sinon.stub().resolves(), + } + + currentActiveConnection = mockIamConnection + + // Override getContext stub to return true for IAM mode + const getContextModule = require('../../../../shared/vscode/setContext') + const existingStub = getContextModule.getContext as sinon.SinonStub + existingStub.withArgs('aws.smus.isIamMode').returns(true) + }) + + it('should display profile name with session name for IAM connection', async function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(true) + mockAuthProvider.getSessionName = sinon.stub().resolves('my-session-name') + mockAuthProvider.getIamPrincipalArn = sinon + .stub() + .resolves('arn:aws:sts::123456789012:assumed-role/MyRole/my-session-name') + + const treeItem = await authInfoNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Connected with profile: test-profile (session: my-session-name)') + assert.strictEqual(treeItem.description, 'us-west-2') + }) + + it('should display profile name without session name when unavailable', async function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(true) + mockAuthProvider.getSessionName = sinon.stub().resolves(undefined) + mockAuthProvider.getIamPrincipalArn = sinon.stub().resolves(undefined) + + const treeItem = await authInfoNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Connected with profile: test-profile') + assert.strictEqual(treeItem.description, 'us-west-2') + }) + + it('should include session name and role ARN in tooltip when available', async function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(true) + mockAuthProvider.getSessionName = sinon.stub().resolves('my-session-name') + mockAuthProvider.getIamPrincipalArn = sinon + .stub() + .resolves('arn:aws:sts::123456789012:assumed-role/MyRole/my-session-name') + + const treeItem = await authInfoNode.getTreeItem() + const tooltip = treeItem.tooltip as string + + assert.ok(tooltip.includes('Connected to SageMaker Unified Studio')) + assert.ok(tooltip.includes('Profile: test-profile')) + assert.ok(tooltip.includes('Region: us-west-2')) + assert.ok(tooltip.includes('Session: my-session-name')) + assert.ok(tooltip.includes('Role ARN: arn:aws:sts::123456789012:assumed-role/MyRole/my-session-name')) + assert.ok(tooltip.includes('Status: Connected')) + }) + + it('should not include session name or role ARN in tooltip when unavailable', async function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(true) + mockAuthProvider.getSessionName = sinon.stub().resolves(undefined) + mockAuthProvider.getIamPrincipalArn = sinon.stub().resolves(undefined) + + const treeItem = await authInfoNode.getTreeItem() + const tooltip = treeItem.tooltip as string + + assert.ok(tooltip.includes('Connected to SageMaker Unified Studio')) + assert.ok(tooltip.includes('Profile: test-profile')) + assert.ok(tooltip.includes('Region: us-west-2')) + assert.ok(!tooltip.includes('Session:')) + assert.ok(!tooltip.includes('Role ARN:')) + assert.ok(tooltip.includes('Status: Connected')) + }) + + it('should handle getSessionName errors gracefully', async function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(true) + mockAuthProvider.getSessionName = sinon.stub().resolves(undefined) // Return undefined instead of rejecting + mockAuthProvider.getIamPrincipalArn = sinon.stub().resolves(undefined) + + // Should not throw, just display without session name + const treeItem = await authInfoNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Connected with profile: test-profile') + assert.strictEqual(treeItem.description, 'us-west-2') + }) + + it('should display expired IAM connection with profile name', async function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(false) + mockAuthProvider.getSessionName = sinon.stub().resolves('my-session-name') + + const treeItem = await authInfoNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Profile: test-profile (Expired) - Click to reauthenticate') + assert.strictEqual(treeItem.description, 'us-west-2') + + // Check icon + assert.ok(treeItem.iconPath instanceof vscode.ThemeIcon) + assert.strictEqual((treeItem.iconPath as vscode.ThemeIcon).id, 'warning') + + // Should have reauthenticate command + assert.ok(treeItem.command) + assert.strictEqual(treeItem.command.command, 'aws.smus.reauthenticate') + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode.test.ts new file mode 100644 index 00000000000..d1b05a547e6 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode.test.ts @@ -0,0 +1,110 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import * as vscode from 'vscode' +import { SageMakerUnifiedStudioComputeNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode' +import { SageMakerUnifiedStudioProjectNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode' +import { SageMakerUnifiedStudioSpacesParentNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode' +import { SagemakerClient } from '../../../../shared/clients/sagemaker' +import { SmusAuthenticationProvider } from '../../../../sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider' +import * as setContext from '../../../../shared/vscode/setContext' + +describe('SageMakerUnifiedStudioComputeNode', function () { + let computeNode: SageMakerUnifiedStudioComputeNode + let mockParent: SageMakerUnifiedStudioProjectNode + let mockExtensionContext: vscode.ExtensionContext + let mockAuthProvider: SmusAuthenticationProvider + let mockSagemakerClient: SagemakerClient + + beforeEach(function () { + mockParent = { + getProject: sinon.stub(), + } as any + + mockExtensionContext = { + subscriptions: [], + extensionUri: vscode.Uri.file('/test'), + } as any + + mockAuthProvider = {} as any + mockSagemakerClient = {} as any + + computeNode = new SageMakerUnifiedStudioComputeNode( + mockParent, + mockExtensionContext, + mockAuthProvider, + mockSagemakerClient + ) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('constructor', function () { + it('creates instance with correct properties', function () { + assert.strictEqual(computeNode.id, 'smusComputeNode') + assert.strictEqual(computeNode.resource, computeNode) + }) + }) + + describe('getTreeItem', function () { + it('returns correct tree item', async function () { + const treeItem = await computeNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Compute') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Expanded) + assert.strictEqual(treeItem.contextValue, 'smusComputeNode') + assert.ok(treeItem.iconPath) + }) + }) + + describe('getChildren', function () { + it('returns empty array when no project is selected', async function () { + ;(mockParent.getProject as sinon.SinonStub).returns(undefined) + + const children = await computeNode.getChildren() + + assert.deepStrictEqual(children, []) + }) + + it('returns connection nodes and spaces node when project is selected (non-IAM mode)', async function () { + const mockProject = { id: 'project-123', name: 'Test Project' } + ;(mockParent.getProject as sinon.SinonStub).returns(mockProject) + + // Mock IAM mode to be false + sinon.stub(setContext, 'getContext').returns(false) + + const children = await computeNode.getChildren() + + assert.strictEqual(children.length, 3) + assert.strictEqual(children[0].id, 'Data warehouse') + assert.strictEqual(children[1].id, 'Data processing') + assert.ok(children[2] instanceof SageMakerUnifiedStudioSpacesParentNode) + }) + + it('returns only spaces node when project is selected (IAM mode)', async function () { + const mockProject = { id: 'project-123', name: 'Test Project' } + ;(mockParent.getProject as sinon.SinonStub).returns(mockProject) + + // Mock IAM mode to be true + sinon.stub(setContext, 'getContext').returns(true) + + const children = await computeNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.ok(children[0] instanceof SageMakerUnifiedStudioSpacesParentNode) + }) + }) + + describe('getParent', function () { + it('returns parent node', function () { + const parent = computeNode.getParent() + assert.strictEqual(parent, mockParent) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionNode.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionNode.test.ts new file mode 100644 index 00000000000..a85d63302a6 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionNode.test.ts @@ -0,0 +1,144 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import * as vscode from 'vscode' +import { SageMakerUnifiedStudioConnectionNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionNode' +import { SageMakerUnifiedStudioConnectionParentNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionParentNode' +import { ConnectionType, ConnectionSummary } from '@aws-sdk/client-datazone' +import { getLogger } from '../../../../shared/logger/logger' + +describe('SageMakerUnifiedStudioConnectionNode', function () { + let connectionNode: SageMakerUnifiedStudioConnectionNode + let mockParent: sinon.SinonStubbedInstance + + const mockRedshiftConnection: ConnectionSummary = { + connectionId: 'conn-1', + name: 'Test Redshift Connection', + type: ConnectionType.REDSHIFT, + environmentId: 'env-1', + domainId: 'domain-1', + domainUnitId: 'unit-1', + physicalEndpoints: [], + props: { + redshiftProperties: { + jdbcUrl: 'jdbc:redshift://test-cluster:5439/testdb', + }, + }, + } + + const mockSparkConnection: ConnectionSummary = { + connectionId: 'conn-2', + name: 'Test Spark Connection', + type: ConnectionType.SPARK, + environmentId: 'env-2', + domainId: 'domain-2', + domainUnitId: 'unit-2', + physicalEndpoints: [], + props: { + sparkGlueProperties: { + glueVersion: '4.0', + workerType: 'G.1X', + numberOfWorkers: 2, + idleTimeout: 30, + }, + }, + } + + beforeEach(function () { + mockParent = {} as any + sinon.stub(getLogger(), 'debug') + }) + + afterEach(function () { + sinon.restore() + }) + + describe('constructor', function () { + it('creates instance with correct properties for Redshift connection', function () { + connectionNode = new SageMakerUnifiedStudioConnectionNode(mockParent as any, mockRedshiftConnection) + + assert.strictEqual(connectionNode.id, 'Test Redshift Connection') + assert.strictEqual(connectionNode.resource, connectionNode) + assert.strictEqual(connectionNode.contextValue, 'SageMakerUnifiedStudioConnectionNode') + }) + + it('creates instance with empty id when connection name is undefined', function () { + const connectionWithoutName = { ...mockRedshiftConnection, name: undefined } + connectionNode = new SageMakerUnifiedStudioConnectionNode(mockParent as any, connectionWithoutName) + + assert.strictEqual(connectionNode.id, '') + }) + }) + + describe('getTreeItem', function () { + it('returns correct tree item for Redshift connection', async function () { + connectionNode = new SageMakerUnifiedStudioConnectionNode(mockParent as any, mockRedshiftConnection) + + const treeItem = await connectionNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Test Redshift Connection') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.strictEqual(treeItem.contextValue, 'SageMakerUnifiedStudioConnectionNode') + assert.ok(treeItem.tooltip instanceof vscode.MarkdownString) + }) + + it('returns correct tree item for Spark connection', async function () { + connectionNode = new SageMakerUnifiedStudioConnectionNode(mockParent as any, mockSparkConnection) + + const treeItem = await connectionNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Test Spark Connection') + assert.ok(treeItem.tooltip instanceof vscode.MarkdownString) + }) + }) + + describe('tooltip generation', function () { + it('generates correct tooltip for Redshift connection', async function () { + connectionNode = new SageMakerUnifiedStudioConnectionNode(mockParent as any, mockRedshiftConnection) + + const treeItem = await connectionNode.getTreeItem() + const tooltip = (treeItem.tooltip as vscode.MarkdownString).value + + assert(tooltip.includes('REDSHIFT')) + assert(tooltip.includes('env-1')) + assert(tooltip.includes('jdbc:redshift://test-cluster:5439/testdb')) + }) + + it('generates correct tooltip for Spark connection', async function () { + connectionNode = new SageMakerUnifiedStudioConnectionNode(mockParent as any, mockSparkConnection) + + const treeItem = await connectionNode.getTreeItem() + const tooltip = (treeItem.tooltip as vscode.MarkdownString).value + + assert(tooltip.includes('SPARK')) + assert(tooltip.includes('4.0')) + assert(tooltip.includes('G.1X')) + assert(tooltip.includes('2')) + assert(tooltip.includes('30')) + }) + + it('generates empty tooltip for unknown connection type', async function () { + const unknownConnection = { ...mockRedshiftConnection, type: 'UNKNOWN' as ConnectionType } + connectionNode = new SageMakerUnifiedStudioConnectionNode(mockParent as any, unknownConnection) + + const treeItem = await connectionNode.getTreeItem() + const tooltip = (treeItem.tooltip as vscode.MarkdownString).value + + assert.strictEqual(tooltip, '') + }) + }) + + describe('getParent', function () { + it('returns the parent node', function () { + connectionNode = new SageMakerUnifiedStudioConnectionNode(mockParent as any, mockRedshiftConnection) + + const parent = connectionNode.getParent() + + assert.strictEqual(parent, mockParent) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionParentNode.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionParentNode.test.ts new file mode 100644 index 00000000000..18778c52664 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionParentNode.test.ts @@ -0,0 +1,254 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import * as vscode from 'vscode' +import { SageMakerUnifiedStudioConnectionParentNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionParentNode' +import { SageMakerUnifiedStudioComputeNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode' +import { SageMakerUnifiedStudioConnectionNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionNode' +import { DataZoneClient } from '../../../../sagemakerunifiedstudio/shared/client/datazoneClient' + +import { ConnectionType, ListConnectionsCommandOutput, ConnectionSummary } from '@aws-sdk/client-datazone' +import { getLogger } from '../../../../shared/logger/logger' + +describe('SageMakerUnifiedStudioConnectionParentNode', function () { + let connectionParentNode: SageMakerUnifiedStudioConnectionParentNode + let mockComputeNode: sinon.SinonStubbedInstance + + let mockDataZoneClient: sinon.SinonStubbedInstance + + const mockProject = { + id: 'project-123', + domainId: 'domain-123', + } + + const mockConnectionsOutput: ListConnectionsCommandOutput = { + items: [ + { + connectionId: 'conn-1', + name: 'Test Connection 1', + type: ConnectionType.REDSHIFT, + environmentId: 'env-1', + } as ConnectionSummary, + { + connectionId: 'conn-2', + name: 'Test Connection 2', + type: ConnectionType.REDSHIFT, + environmentId: 'env-2', + } as ConnectionSummary, + ], + $metadata: {}, + } + + beforeEach(function () { + // Create mock objects + mockDataZoneClient = { + fetchConnections: sinon.stub(), + } as any + + mockComputeNode = { + authProvider: { + getDerCredentialsProvider: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + getDomainId: sinon.stub().returns('domain-123'), + getDomainRegion: sinon.stub().returns('us-east-1'), + } as any, + parent: { + project: mockProject, + } as any, + } as any + + // Stub static methods + sinon.stub(DataZoneClient, 'createWithCredentials').resolves(mockDataZoneClient as any) + sinon.stub(getLogger(), 'debug') + + connectionParentNode = new SageMakerUnifiedStudioConnectionParentNode( + mockComputeNode as any, + ConnectionType.REDSHIFT, + 'Data warehouse' + ) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('constructor', function () { + it('creates instance with correct properties', function () { + assert.strictEqual(connectionParentNode.id, 'Data warehouse') + assert.strictEqual(connectionParentNode.resource, connectionParentNode) + assert.strictEqual(connectionParentNode.contextValue, 'SageMakerUnifiedStudioConnectionParentNode') + }) + }) + + describe('getTreeItem', function () { + it('returns correct tree item', async function () { + const treeItem = await connectionParentNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Data warehouse') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) + assert.strictEqual(treeItem.contextValue, 'SageMakerUnifiedStudioConnectionParentNode') + }) + }) + + describe('getChildren', function () { + it('returns connection nodes when connections exist', async function () { + mockDataZoneClient.fetchConnections.resolves(mockConnectionsOutput) + + const children = await connectionParentNode.getChildren() + + assert.strictEqual(children.length, 2) + assert(children[0] instanceof SageMakerUnifiedStudioConnectionNode) + assert(children[1] instanceof SageMakerUnifiedStudioConnectionNode) + + // Verify fetchConnections was called with correct parameters + assert( + mockDataZoneClient.fetchConnections.calledOnceWith( + mockProject.domainId, + mockProject.id, + ConnectionType.REDSHIFT + ) + ) + }) + + it('returns no connections node when no connections exist', async function () { + const emptyOutput: ListConnectionsCommandOutput = { items: [], $metadata: {} } + mockDataZoneClient.fetchConnections.resolves(emptyOutput) + + const children = await connectionParentNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].id, 'smusNoConnections') + const treeItem = await children[0].getTreeItem() + assert.strictEqual(treeItem.label, '[No connections found]') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + }) + + it('returns no connections node when connections items is undefined', async function () { + const undefinedOutput: ListConnectionsCommandOutput = { items: undefined, $metadata: {} } + mockDataZoneClient.fetchConnections.resolves(undefinedOutput) + + const children = await connectionParentNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].id, 'smusNoConnections') + }) + + it('handles missing project information gracefully', async function () { + const nodeWithoutProject = new SageMakerUnifiedStudioConnectionParentNode( + { + authProvider: { + getDerCredentialsProvider: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + getDomainId: sinon.stub().returns('domain-123'), + getDomainRegion: sinon.stub().returns('us-east-1'), + } as any, + parent: { + project: undefined, + } as any, + } as any, + ConnectionType.SPARK, + 'Data processing' + ) + + mockDataZoneClient.fetchConnections.resolves({ items: [], $metadata: {} }) + + const children = await nodeWithoutProject.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].id, 'smusNoConnections') + assert(mockDataZoneClient.fetchConnections.calledOnceWith(undefined, undefined, ConnectionType.SPARK)) + }) + }) + + describe('getParent', function () { + it('returns the parent compute node', function () { + const parent = connectionParentNode.getParent() + assert.strictEqual(parent, mockComputeNode) + }) + }) + + describe('error handling', function () { + it('handles DataZoneClient.getInstance error', async function () { + sinon.restore() + sinon.stub(DataZoneClient, 'createWithCredentials').rejects(new Error('Client error')) + sinon.stub(getLogger(), 'debug') + + try { + await connectionParentNode.getChildren() + assert.fail('Expected error to be thrown') + } catch (error) { + assert.strictEqual((error as Error).message, 'Client error') + } + }) + + it('handles fetchConnections error', async function () { + mockDataZoneClient.fetchConnections.rejects(new Error('Fetch error')) + + try { + await connectionParentNode.getChildren() + assert.fail('Expected error to be thrown') + } catch (error) { + assert.strictEqual((error as Error).message, 'Fetch error') + } + }) + }) + + describe('connections property', function () { + it('sets connections property after getChildren call', async function () { + mockDataZoneClient.fetchConnections.resolves(mockConnectionsOutput) + + await connectionParentNode.getChildren() + + assert.strictEqual(connectionParentNode.connections, mockConnectionsOutput) + }) + }) + + describe('different connection types', function () { + it('works with SPARK connection type', async function () { + const sparkNode = new SageMakerUnifiedStudioConnectionParentNode( + mockComputeNode as any, + ConnectionType.SPARK, + 'Spark connections' + ) + + const sparkOutput = { + items: [ + { + connectionId: 'spark-1', + name: 'Spark Connection', + type: ConnectionType.SPARK, + environmentId: 'env-spark', + } as ConnectionSummary, + ], + $metadata: {}, + } + + mockDataZoneClient.fetchConnections.resolves(sparkOutput) + + const children = await sparkNode.getChildren() + + assert.strictEqual(children.length, 1) + assert( + mockDataZoneClient.fetchConnections.calledWith( + mockProject.domainId, + mockProject.id, + ConnectionType.SPARK + ) + ) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioDataNode.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioDataNode.test.ts new file mode 100644 index 00000000000..3d3ad01108c --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioDataNode.test.ts @@ -0,0 +1,249 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { SageMakerUnifiedStudioDataNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioDataNode' +import { SageMakerUnifiedStudioProjectNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode' +import { DataZoneClient, DataZoneProject } from '../../../../sagemakerunifiedstudio/shared/client/datazoneClient' +import { SmusAuthenticationProvider } from '../../../../sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider' +import * as s3Strategy from '../../../../sagemakerunifiedstudio/explorer/nodes/s3Strategy' +import * as redshiftStrategy from '../../../../sagemakerunifiedstudio/explorer/nodes/redshiftStrategy' +import * as lakehouseStrategy from '../../../../sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy' +import * as setContext from '../../../../shared/vscode/setContext' + +describe('SageMakerUnifiedStudioDataNode', function () { + let sandbox: sinon.SinonSandbox + let dataNode: SageMakerUnifiedStudioDataNode + let mockParent: sinon.SinonStubbedInstance + let mockDataZoneClient: sinon.SinonStubbedInstance + let mockAuthProvider: sinon.SinonStubbedInstance + let mockProjectCredentialProvider: any + + const mockProject: DataZoneProject = { + id: 'project-123', + name: 'Test Project', + domainId: 'domain-123', + } + + const mockCredentials = { + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + $metadata: {}, + } + + beforeEach(function () { + sandbox = sinon.createSandbox() + + mockParent = { + getProject: sandbox.stub().returns(mockProject), + } as any + + mockProjectCredentialProvider = { + getCredentials: sandbox.stub().resolves(mockCredentials), + } + + mockAuthProvider = { + getProjectCredentialProvider: sandbox.stub().resolves(mockProjectCredentialProvider), + getConnectionCredentialsProvider: sandbox.stub().resolves(mockProjectCredentialProvider), + getDomainRegion: sandbox.stub().returns('us-east-1'), + getDomainId: sandbox.stub().returns('domain-123'), + getDerCredentialsProvider: sandbox.stub().resolves({ + getCredentials: sandbox.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + } as any + + mockDataZoneClient = { + getInstance: sandbox.stub(), + getProjectDefaultEnvironmentCreds: sandbox.stub(), + listConnections: sandbox.stub(), + getConnection: sandbox.stub(), + getRegion: sandbox.stub().returns('us-east-1'), + } as any + + sandbox.stub(DataZoneClient, 'createWithCredentials').returns(mockDataZoneClient as any) + sandbox.stub(SmusAuthenticationProvider, 'fromContext').returns(mockAuthProvider as any) + sandbox.stub(s3Strategy, 'createS3ConnectionNode').returns({ + id: 's3-node', + getChildren: () => Promise.resolve([]), + getTreeItem: () => ({}) as any, + getParent: () => undefined, + } as any) + sandbox.stub(s3Strategy, 'createS3AccessGrantNodes').resolves([]) + sandbox.stub(redshiftStrategy, 'createRedshiftConnectionNode').returns({ + id: 'redshift-node', + getChildren: () => Promise.resolve([]), + getTreeItem: () => ({}) as any, + getParent: () => undefined, + } as any) + sandbox.stub(lakehouseStrategy, 'createLakehouseConnectionNode').returns({ + id: 'lakehouse-node', + getChildren: () => Promise.resolve([]), + getTreeItem: () => ({}) as any, + getParent: () => undefined, + } as any) + + dataNode = new SageMakerUnifiedStudioDataNode(mockParent as any) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('constructor', function () { + it('should initialize with correct properties', function () { + assert.strictEqual(dataNode.id, 'smusDataExplorer') + assert.deepStrictEqual(dataNode.resource, {}) + }) + + it('should initialize with provided children', function () { + const initialChildren = [{ id: 'child1' } as any] + const nodeWithChildren = new SageMakerUnifiedStudioDataNode(mockParent as any, initialChildren) + // Children should be cached + assert.ok(nodeWithChildren) + }) + }) + + describe('getTreeItem', function () { + it('should return correct tree item', function () { + const treeItem = dataNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Data') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) + assert.strictEqual(treeItem.contextValue, 'dataFolder') + }) + }) + + describe('getParent', function () { + it('should return parent node', function () { + assert.strictEqual(dataNode.getParent(), mockParent) + }) + }) + + describe('getChildren', function () { + it('should return cached children if available', async function () { + const initialChildren = [{ id: 'cached' } as any] + const nodeWithCache = new SageMakerUnifiedStudioDataNode(mockParent as any, initialChildren) + + const children = await nodeWithCache.getChildren() + assert.strictEqual(children, initialChildren) + }) + + it('should return error node when no project available', async function () { + mockParent.getProject.returns(undefined) + + const children = await dataNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.ok(children[0].id.startsWith('smusDataExplorer-error-project-')) + }) + + it('should return error node when credentials are missing', async function () { + mockProjectCredentialProvider.getCredentials.resolves(undefined) + + const children = await dataNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.ok(children[0].id.startsWith('smusDataExplorer-error-connections-')) + }) + + it('should return placeholder when no connections found', async function () { + mockDataZoneClient.listConnections.resolves([]) + + const children = await dataNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].resource, '[No data found]') + }) + + it('should create Bucket parent node and Redshift nodes for connections', async function () { + const mockConnections = [ + { connectionId: 's3-conn', type: 'S3', name: 's3-connection' }, + { connectionId: 'redshift-conn', type: 'REDSHIFT', name: 'redshift-connection' }, + ] + + // Mock IAM mode to be false so Redshift connections are included + sandbox.stub(setContext, 'getContext').returns(false) + + mockDataZoneClient.listConnections.resolves(mockConnections as any) + mockDataZoneClient.getConnection + .onFirstCall() + .resolves({ + location: { awsRegion: 'us-east-1', awsAccountId: '' }, + connectionCredentials: mockCredentials, + connectionId: '', + name: '', + type: '', + domainId: '', + projectId: '', + }) + .onSecondCall() + .resolves({ + location: { awsRegion: 'us-east-1', awsAccountId: '' }, + connectionCredentials: mockCredentials, + connectionId: '', + name: '', + type: '', + domainId: '', + projectId: '', + }) + + const children = await dataNode.getChildren() + + // Should have Bucket parent node and Redshift node + assert.strictEqual(children.length, 2) + + // Check for Bucket parent node + const bucketNode = children.find((child) => child.id === 'bucket-parent') + assert.ok(bucketNode, 'Should have bucket parent node') + + // Verify Bucket node has correct tree item + const bucketTreeItem = await bucketNode!.getTreeItem() + assert.strictEqual(bucketTreeItem.label, 'Buckets') + assert.strictEqual(bucketTreeItem.contextValue, 'bucketFolder') + + // Verify S3 nodes are created when Bucket node is expanded + await bucketNode!.getChildren!() + assert.ok((s3Strategy.createS3ConnectionNode as sinon.SinonStub).calledOnce) + + assert.ok((redshiftStrategy.createRedshiftConnectionNode as sinon.SinonStub).calledOnce) + }) + + it('should handle connection detail errors gracefully', async function () { + const mockConnections = [{ connectionId: 's3-conn', type: 'S3', name: 's3-connection' }] + + mockDataZoneClient.listConnections.resolves(mockConnections as any) + + const children = await dataNode.getChildren() + + // Should have Bucket parent node even with connection errors + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].id, 'bucket-parent') + + // Mock connection credentials provider to reject when bucket is expanded + mockAuthProvider.getConnectionCredentialsProvider.rejects(new Error('Connection error')) + + // Error should occur when expanding the Bucket node + const bucketChildren = await children[0].getChildren!() + assert.strictEqual(bucketChildren.length, 1) + assert.ok(bucketChildren[0].id.startsWith('smusDataExplorer-error-s3-')) + }) + + it('should return error node when general error occurs', async function () { + mockAuthProvider.getProjectCredentialProvider.rejects(new Error('General error')) + + const children = await dataNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.ok(children[0].id.startsWith('smusDataExplorer-error-connections-')) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode.test.ts new file mode 100644 index 00000000000..8c71b102064 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode.test.ts @@ -0,0 +1,344 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import * as vscode from 'vscode' +import { SageMakerUnifiedStudioProjectNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode' +import { DataZoneClient, DataZoneProject } from '../../../../sagemakerunifiedstudio/shared/client/datazoneClient' +import { getLogger } from '../../../../shared/logger/logger' +import { telemetry } from '../../../../shared/telemetry/telemetry' +import { SagemakerClient } from '../../../../shared/clients/sagemaker' +import { SageMakerUnifiedStudioDataNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioDataNode' +import { SageMakerUnifiedStudioComputeNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode' +import * as vscodeUtils from '../../../../shared/vscode/setContext' +import { createMockExtensionContext } from '../../testUtils' + +describe('SageMakerUnifiedStudioProjectNode', function () { + let projectNode: SageMakerUnifiedStudioProjectNode + let mockDataZoneClient: sinon.SinonStubbedInstance + + const mockProject: DataZoneProject = { + id: 'project-123', + name: 'Test Project', + description: 'Test Description', + domainId: 'domain-123', + } + + beforeEach(function () { + // Create mock parent + const mockParent = {} as any + + // Create mock auth provider + const mockAuthProvider = { + activeConnection: { domainId: 'test-domain', ssoRegion: 'us-west-2' }, + invalidateAllProjectCredentialsInCache: sinon.stub(), + getProjectCredentialProvider: sinon.stub(), + getDomainRegion: sinon.stub().returns('us-west-2'), + getDomainAccountId: sinon.stub().resolves('123456789012'), + getDomainId: sinon.stub().returns('test-domain'), + getDerCredentialsProvider: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + } as any + + // Create mock extension context + const mockExtensionContext = createMockExtensionContext() + + projectNode = new SageMakerUnifiedStudioProjectNode(mockParent, mockAuthProvider, mockExtensionContext) + + sinon.stub(getLogger(), 'info') + sinon.stub(getLogger(), 'warn') + + // Stub telemetry + sinon.stub(telemetry, 'record') + + // Create mock DataZone client + mockDataZoneClient = { + getProjectDefaultEnvironmentCreds: sinon.stub(), + getUserId: sinon.stub(), + fetchAllProjectMemberships: sinon.stub(), + getDomainId: sinon.stub().returns('test-domain-id'), + getToolingEnvironmentId: sinon.stub(), + getEnvironmentDetails: sinon.stub(), + getToolingEnvironment: sinon.stub(), + } as any + + // Stub DataZoneClient static methods + sinon.stub(DataZoneClient, 'createWithCredentials').returns(mockDataZoneClient as any) + + // Stub SagemakerClient constructor + sinon.stub(SagemakerClient.prototype, 'dispose') + + // Stub child node constructors to prevent actual instantiation + sinon.stub(SageMakerUnifiedStudioDataNode.prototype, 'constructor' as any).returns({}) + sinon.stub(SageMakerUnifiedStudioComputeNode.prototype, 'constructor' as any).returns({}) + + // Stub getContext to return false for SMUS space environment + sinon.stub(vscodeUtils, 'getContext').returns(false) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('constructor', function () { + it('creates instance with correct properties', function () { + assert.strictEqual(projectNode.id, 'smusProjectNode') + assert.strictEqual(projectNode.resource, projectNode) + }) + }) + + describe('getTreeItem', function () { + it('returns correct tree item when no project is selected', async function () { + const treeItem = await projectNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Select a project') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Expanded) + assert.strictEqual(treeItem.contextValue, 'smusProjectSelectPicker') + assert.ok(treeItem.command) + assert.strictEqual(treeItem.command?.command, 'aws.smus.projectView') + }) + + it('returns correct tree item when project is selected', async function () { + await projectNode.setProject(mockProject) + const treeItem = await projectNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Project: ' + mockProject.name) + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Expanded) + assert.strictEqual(treeItem.contextValue, 'smusSelectedProject') + assert.strictEqual(treeItem.tooltip, `Project: ${mockProject.name}\nID: ${mockProject.id}`) + }) + }) + + describe('getParent', function () { + it('returns parent node', function () { + const parent = projectNode.getParent() + assert.ok(parent) + }) + }) + + describe('setProject', function () { + it('updates the project and calls cleanupProjectResources', async function () { + const cleanupSpy = sinon.spy(projectNode as any, 'cleanupProjectResources') + await projectNode.setProject(mockProject) + assert.strictEqual(projectNode['project'], mockProject) + assert(cleanupSpy.calledOnce) + }) + }) + + describe('clearProject', function () { + it('clears the project, calls cleanupProjectResources and fires change event', async function () { + await projectNode.setProject(mockProject) + const cleanupSpy = sinon.spy(projectNode as any, 'cleanupProjectResources') + const emitterSpy = sinon.spy(projectNode['onDidChangeEmitter'], 'fire') + + await projectNode.clearProject() + + assert.strictEqual(projectNode['project'], undefined) + assert(cleanupSpy.calledOnce) + assert(emitterSpy.calledOnce) + }) + }) + + describe('getProject', function () { + it('returns undefined when no project is set', function () { + assert.strictEqual(projectNode.getProject(), undefined) + }) + + it('returns project when set', async function () { + await projectNode.setProject(mockProject) + assert.strictEqual(projectNode.getProject(), mockProject) + }) + }) + + describe('refreshNode', function () { + it('fires change event', async function () { + const emitterSpy = sinon.spy(projectNode['onDidChangeEmitter'], 'fire') + await projectNode.refreshNode() + assert(emitterSpy.calledOnce) + }) + }) + + describe('getChildren', function () { + it('returns empty array when no project is selected', async function () { + const children = await projectNode.getChildren() + assert.deepStrictEqual(children, []) + }) + + it('returns data and compute nodes when project is selected and user has access', async function () { + await projectNode.setProject(mockProject) + const mockCredProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + projectNode['authProvider'].getProjectCredentialProvider = sinon.stub().resolves(mockCredProvider) + + // Mock getToolingEnvironment method + mockDataZoneClient.getToolingEnvironment.resolves({ + id: 'env-123', + awsAccountRegion: 'us-east-1', + projectId: undefined, + domainId: undefined, + createdBy: undefined, + name: undefined, + provider: undefined, + $metadata: {}, + }) + + const children = await projectNode.getChildren() + assert.strictEqual(children.length, 2) + }) + + it('returns access denied message when user does not have project access', async function () { + await projectNode.setProject(mockProject) + + // Mock access check to return false by throwing AccessDeniedException + const accessError = new Error('Access denied') + accessError.name = 'AccessDeniedException' + projectNode['authProvider'].getProjectCredentialProvider = sinon.stub().rejects(accessError) + + const children = await projectNode.getChildren() + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].id, 'smusProjectAccessDenied') + + const treeItem = await children[0].getTreeItem() + assert.strictEqual(treeItem.label, 'You do not have access to this project. Contact your administrator.') + }) + + it('throws error when initializeSagemakerClient fails', async function () { + await projectNode.setProject(mockProject) + const credError = new Error('Failed to initialize SageMaker client') + + // First call succeeds for access check, second call fails for initializeSagemakerClient + const mockCredProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + projectNode['authProvider'].getProjectCredentialProvider = sinon + .stub() + .onFirstCall() + .resolves(mockCredProvider) + .onSecondCall() + .rejects(credError) + + // Mock getToolingEnvironment method + mockDataZoneClient.getToolingEnvironment.resolves({ + id: 'env-123', + awsAccountRegion: 'us-east-1', + projectId: undefined, + domainId: undefined, + createdBy: undefined, + name: undefined, + provider: undefined, + $metadata: {}, + }) + + await assert.rejects(async () => await projectNode.getChildren(), credError) + }) + }) + + describe('initializeSagemakerClient', function () { + it('throws error when no project is selected', async function () { + await assert.rejects( + async () => await projectNode['initializeSagemakerClient']('us-east-1'), + /No project selected for initializing SageMaker client/ + ) + }) + + it('creates SagemakerClient with project credentials', async function () { + await projectNode.setProject(mockProject) + const mockCredProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + projectNode['authProvider'].getProjectCredentialProvider = sinon.stub().resolves(mockCredProvider) + + const client = await projectNode['initializeSagemakerClient']('us-east-1') + assert.ok(client instanceof SagemakerClient) + assert( + (projectNode['authProvider'].getProjectCredentialProvider as sinon.SinonStub).calledWith(mockProject.id) + ) + }) + }) + + describe('checkProjectCredsAccess', function () { + it('returns true when user has project access', async function () { + const mockCredProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + projectNode['authProvider'].getProjectCredentialProvider = sinon.stub().resolves(mockCredProvider) + + const hasAccess = await projectNode['checkProjectCredsAccess']('project-123') + assert.strictEqual(hasAccess, true) + }) + + it('returns false when user does not have project access', async function () { + const accessError = new Error('Access denied') + accessError.name = 'AccessDeniedException' + projectNode['authProvider'].getProjectCredentialProvider = sinon.stub().rejects(accessError) + + const hasAccess = await projectNode['checkProjectCredsAccess']('project-123') + assert.strictEqual(hasAccess, false) + }) + + it('throws error when getCredentials fails', async function () { + const mockCredProvider = { + getCredentials: sinon.stub().rejects(new Error('Credentials error')), + } + projectNode['authProvider'].getProjectCredentialProvider = sinon.stub().resolves(mockCredProvider) + + await assert.rejects( + async () => await projectNode['checkProjectCredsAccess']('project-123'), + /Credentials error/ + ) + }) + + it('throws error when access check throws non-AccessDeniedException error', async function () { + projectNode['authProvider'].getProjectCredentialProvider = sinon.stub().rejects(new Error('Other error')) + + await assert.rejects(async () => await projectNode['checkProjectCredsAccess']('project-123'), /Other error/) + }) + }) + + describe('cleanupProjectResources', function () { + it('invalidates credentials and disposes existing sagemaker client', async function () { + // Set up existing sagemaker client with mock + const mockClient = { dispose: sinon.stub() } as any + projectNode['sagemakerClient'] = mockClient + + await projectNode['cleanupProjectResources']() + + assert((projectNode['authProvider'].invalidateAllProjectCredentialsInCache as sinon.SinonStub).calledOnce) + assert(mockClient.dispose.calledOnce) + assert.strictEqual(projectNode['sagemakerClient'], undefined) + }) + + it('handles case when no sagemaker client exists', async function () { + projectNode['sagemakerClient'] = undefined + + await projectNode['cleanupProjectResources']() + + assert((projectNode['authProvider'].invalidateAllProjectCredentialsInCache as sinon.SinonStub).calledOnce) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioRootNode.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioRootNode.test.ts new file mode 100644 index 00000000000..facfa867aca --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioRootNode.test.ts @@ -0,0 +1,1115 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import * as vscode from 'vscode' +import { + SageMakerUnifiedStudioRootNode, + selectSMUSProject, +} from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioRootNode' +import { SageMakerUnifiedStudioProjectNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode' +import { DataZoneClient, DataZoneProject } from '../../../../sagemakerunifiedstudio/shared/client/datazoneClient' +import { SageMakerUnifiedStudioAuthInfoNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioAuthInfoNode' +import { SmusAuthenticationProvider } from '../../../../sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider' +import * as pickerPrompter from '../../../../shared/ui/pickerPrompter' +import { getTestWindow } from '../../../shared/vscode/window' +import { assertTelemetry } from '../../../../../src/test/testUtil' +import { createMockExtensionContext, createMockUnauthenticatedAuthProvider } from '../../testUtils' +import { DataZoneCustomClientHelper } from '../../../../sagemakerunifiedstudio/shared/client/datazoneCustomClientHelper' +import { DefaultStsClient } from '../../../../shared/clients/stsClient' +import { SmusUtils, SmusErrorCodes } from '../../../../sagemakerunifiedstudio/shared/smusUtils' +import { ToolkitError } from '../../../../shared/errors' +import * as utils from '../../../../sagemakerunifiedstudio/explorer/nodes/utils' +import * as setContextModule from '../../../../shared/vscode/setContext' + +describe('SmusRootNode', function () { + let rootNode: SageMakerUnifiedStudioRootNode + let mockDataZoneClient: sinon.SinonStubbedInstance + + const testDomainId = 'test-domain-123' + const mockProject: DataZoneProject = { + id: 'project-123', + name: 'Test Project', + description: 'Test Description', + domainId: testDomainId, + } + + /** + * Helper function to verify login and learn more nodes + */ + async function verifyLoginAndLearnMoreNodes(children: any[]) { + assert.strictEqual(children.length, 2) + assert.strictEqual(children[0].id, 'smusLogin') + assert.strictEqual(children[1].id, 'smusLearnMore') + + // Check login node + const loginTreeItem = await children[0].getTreeItem() + assert.strictEqual(loginTreeItem.label, 'Sign in to get started') + assert.strictEqual(loginTreeItem.contextValue, 'sageMakerUnifiedStudioLogin') + assert.deepStrictEqual(loginTreeItem.command, { + command: 'aws.smus.login', + title: 'Sign in to SageMaker Unified Studio', + }) + + // Check learn more node + const learnMoreTreeItem = await children[1].getTreeItem() + assert.strictEqual(learnMoreTreeItem.label, 'Learn more about SageMaker Unified Studio') + assert.strictEqual(learnMoreTreeItem.contextValue, 'sageMakerUnifiedStudioLearnMore') + assert.deepStrictEqual(learnMoreTreeItem.command, { + command: 'aws.smus.learnMore', + title: 'Learn more about SageMaker Unified Studio', + }) + } + + beforeEach(function () { + // Create mock extension context + const mockExtensionContext = createMockExtensionContext() + + // Create a mock auth provider + const mockAuthProvider = { + isConnected: sinon.stub().returns(true), + isConnectionValid: sinon.stub().returns(true), + activeConnection: { domainId: testDomainId, ssoRegion: 'us-west-2' }, + onDidChange: sinon.stub().returns({ dispose: sinon.stub() }), + } as any + + rootNode = new SageMakerUnifiedStudioRootNode(mockAuthProvider, mockExtensionContext) + + // Mock domain ID is handled by the mock auth provider + + // Create mock DataZone client + mockDataZoneClient = { + getDomainId: sinon.stub().returns(testDomainId), + listProjects: sinon.stub(), + } as any + + // Stub DataZoneClient static methods + sinon.stub(DataZoneClient, 'createWithCredentials').returns(mockDataZoneClient as any) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('constructor', function () { + it('should initialize id and resource properties', function () { + // Create a mock auth provider + const mockAuthProvider = { + isConnected: sinon.stub().returns(true), + isConnectionValid: sinon.stub().returns(true), + activeConnection: { domainId: testDomainId, ssoRegion: 'us-west-2' }, + onDidChange: sinon.stub().returns({ dispose: sinon.stub() }), + } as any + + const mockExtensionContext = createMockExtensionContext() + + const node = new SageMakerUnifiedStudioRootNode(mockAuthProvider, mockExtensionContext) + assert.strictEqual(node.id, 'smusRootNode') + assert.strictEqual(node.resource, node) + assert.ok(node.getAuthInfoNode() instanceof SageMakerUnifiedStudioAuthInfoNode) + assert.ok(node.getProjectSelectNode() instanceof SageMakerUnifiedStudioProjectNode) + assert.strictEqual(typeof node.onDidChangeTreeItem, 'function') + assert.strictEqual(typeof node.onDidChangeChildren, 'function') + }) + }) + + describe('getTreeItem', function () { + it('returns correct tree item when authenticated', async function () { + const treeItem = rootNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'SageMaker Unified Studio') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Expanded) + assert.strictEqual(treeItem.contextValue, 'sageMakerUnifiedStudioRoot') + assert.strictEqual(treeItem.description, 'Connected') + assert.ok(treeItem.iconPath) + }) + + it('returns correct tree item when not authenticated', async function () { + // Create a mock auth provider for unauthenticated state + const mockAuthProvider = createMockUnauthenticatedAuthProvider() + const mockExtensionContext = createMockExtensionContext() + + const unauthenticatedNode = new SageMakerUnifiedStudioRootNode(mockAuthProvider, mockExtensionContext) + const treeItem = unauthenticatedNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'SageMaker Unified Studio') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Expanded) + assert.strictEqual(treeItem.contextValue, 'sageMakerUnifiedStudioRoot') + assert.strictEqual(treeItem.description, 'Not authenticated') + assert.ok(treeItem.iconPath) + }) + }) + + describe('getChildren', function () { + it('returns login node when not authenticated (empty domain ID)', async function () { + // Create a mock auth provider for unauthenticated state + const mockAuthProvider = createMockUnauthenticatedAuthProvider() + const mockExtensionContext = createMockExtensionContext() + + const unauthenticatedNode = new SageMakerUnifiedStudioRootNode(mockAuthProvider, mockExtensionContext) + const children = await unauthenticatedNode.getChildren() + await verifyLoginAndLearnMoreNodes(children) + }) + + it('returns login node when DataZone client throws error', async function () { + // Create a mock auth provider that throws an error + const mockAuthProvider = { + isConnected: sinon.stub().throws(new Error('Auth provider error')), + isConnectionValid: sinon.stub().returns(false), + activeConnection: undefined, + onDidChange: sinon.stub().returns({ dispose: sinon.stub() }), + } as any + + const mockExtensionContext = createMockExtensionContext() + + const errorNode = new SageMakerUnifiedStudioRootNode(mockAuthProvider, mockExtensionContext) + const children = await errorNode.getChildren() + await verifyLoginAndLearnMoreNodes(children) + }) + + it('returns root nodes when authenticated', async function () { + mockDataZoneClient.listProjects.resolves({ projects: [mockProject], nextToken: undefined }) + + const children = await rootNode.getChildren() + + assert.strictEqual(children.length, 2) + assert.ok(children[0] instanceof SageMakerUnifiedStudioAuthInfoNode) + assert.ok(children[1] instanceof SageMakerUnifiedStudioProjectNode) + // The first child is the auth info node, the second is the project node + assert.strictEqual(children[0].id, 'smusAuthInfoNode') + assert.strictEqual(children[1].id, 'smusProjectNode') + + assert.strictEqual(children.length, 2) + assert.strictEqual(children[1].id, 'smusProjectNode') + + const treeItem = await children[1].getTreeItem() + assert.strictEqual(treeItem.label, 'Select a project') + assert.strictEqual(treeItem.contextValue, 'smusProjectSelectPicker') + assert.deepStrictEqual(treeItem.command, { + command: 'aws.smus.projectView', + title: 'Select Project', + arguments: [children[1]], + }) + }) + + it('returns auth info node when connection is expired', async function () { + // Create a mock auth provider with expired connection + const mockAuthProvider = { + isConnected: sinon.stub().returns(true), + isConnectionValid: sinon.stub().returns(false), + activeConnection: { + type: 'sso', + domainId: testDomainId, + ssoRegion: 'us-west-2', + domainUrl: 'https://test-domain.datazone.aws.amazon.com', + scopes: ['datazone:domain:access'], + }, + onDidChange: sinon.stub().returns({ dispose: sinon.stub() }), + showReauthenticationPrompt: sinon.stub(), + } as any + + const mockExtensionContext = createMockExtensionContext() + + const expiredNode = new SageMakerUnifiedStudioRootNode(mockAuthProvider, mockExtensionContext) + const children = await expiredNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.ok(children[0] instanceof SageMakerUnifiedStudioAuthInfoNode) + assert.ok(mockAuthProvider.showReauthenticationPrompt.calledOnce) + }) + }) + + describe('refresh', function () { + it('fires change events', function () { + const onDidChangeTreeItemSpy = sinon.spy() + const onDidChangeChildrenSpy = sinon.spy() + + rootNode.onDidChangeTreeItem(onDidChangeTreeItemSpy) + rootNode.onDidChangeChildren(onDidChangeChildrenSpy) + + rootNode.refresh() + + assert(onDidChangeTreeItemSpy.calledOnce) + assert(onDidChangeChildrenSpy.calledOnce) + }) + }) +}) + +describe('SelectSMUSProject', function () { + let mockDataZoneClient: sinon.SinonStubbedInstance + let mockProjectNode: sinon.SinonStubbedInstance + let createQuickPickStub: sinon.SinonStub + let executeCommandStub: sinon.SinonStub + let getContextStub: sinon.SinonStub + let createDZClientStub: sinon.SinonStub + + const testDomainId = 'test-domain-123' + const mockProject: DataZoneProject = { + id: 'project-123', + name: 'Test Project', + description: 'Test Description', + domainId: testDomainId, + updatedAt: new Date(), + } + + const mockProject2: DataZoneProject = { + id: 'project-456', + name: 'Another Project', + description: 'Another Description', + domainId: testDomainId, + updatedAt: new Date(Date.now() - 86400000), // 1 day ago + } + + beforeEach(function () { + // Create mock DataZone client + mockDataZoneClient = { + getDomainId: sinon.stub().returns(testDomainId), + listProjects: sinon.stub(), + fetchAllProjects: sinon.stub(), + } as any + + // Create mock project node + mockProjectNode = { + setProject: sinon.stub(), + getProject: sinon.stub().returns(undefined), + project: undefined, + } as any + + // Stub createDZClientBaseOnDomainMode to return our mock client + createDZClientStub = sinon.stub() + createDZClientStub.resolves(mockDataZoneClient) + sinon.replace(utils, 'createDZClientBaseOnDomainMode', createDZClientStub) + + // Stub SmusAuthenticationProvider + sinon.stub(SmusAuthenticationProvider, 'fromContext').returns({ + isConnected: sinon.stub().returns(true), + isConnectionValid: sinon.stub().returns(true), + activeConnection: { domainId: testDomainId, ssoRegion: 'us-west-2' }, + getDomainAccountId: sinon.stub().resolves('123456789012'), + getDomainId: sinon.stub().returns(testDomainId), + getDomainRegion: sinon.stub().returns('us-west-2'), + getDerCredentialsProvider: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + } as any) + + // Stub getContext to return false for IAM mode by default (non-IAM mode) + getContextStub = sinon.stub() + getContextStub.withArgs('aws.smus.isIamMode').returns(false) + getContextStub.callThrough() + sinon.replace(setContextModule, 'getContext', getContextStub) + + // Stub quickPick - return the project directly (not wrapped in an item) + const mockQuickPick = { + prompt: sinon.stub().resolves(mockProject), + } + createQuickPickStub = sinon.stub(pickerPrompter, 'createQuickPick').returns(mockQuickPick as any) + + // Stub vscode.commands.executeCommand + executeCommandStub = sinon.stub(vscode.commands, 'executeCommand') + }) + + afterEach(function () { + sinon.restore() + }) + + it('fetches all projects and sets the project for first time', async function () { + mockDataZoneClient.fetchAllProjects.resolves([mockProject, mockProject2]) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.strictEqual(result, mockProject) + assert.ok(mockDataZoneClient.fetchAllProjects.calledOnce) + assert.ok(mockDataZoneClient.fetchAllProjects.calledWith()) + assert.ok(createQuickPickStub.calledOnce) + assert.ok(mockProjectNode.setProject.calledOnce) + assert.ok(executeCommandStub.calledWith('aws.smus.rootView.refresh')) + assertTelemetry('smus_accessProject', { + result: 'Succeeded', + smusProjectId: mockProject.id, + }) + }) + + it('filters out GenerativeAIModelGovernanceProject', async function () { + const governanceProject: DataZoneProject = { + id: 'governance-123', + name: 'GenerativeAIModelGovernanceProject', + description: 'Governance project', + domainId: testDomainId, + updatedAt: new Date(), + } + + mockDataZoneClient.fetchAllProjects.resolves([mockProject, governanceProject, mockProject2]) + + await selectSMUSProject(mockProjectNode as any) + + // Verify that the governance project is filtered out + const quickPickCall = createQuickPickStub.getCall(0) + const items = quickPickCall.args[0] + assert.strictEqual(items.length, 2) // Should only have mockProject and mockProject2 + assert.ok(!items.some((item: any) => item.data.name === 'GenerativeAIModelGovernanceProject')) + }) + + it('handles no active connection', async function () { + sinon.restore() + sinon.stub(SmusAuthenticationProvider, 'fromContext').returns({ + activeConnection: undefined, + getDomainId: sinon.stub().returns(undefined), + } as any) + + const result = await selectSMUSProject(mockProjectNode as any) + assert.strictEqual(result, undefined) + + assertTelemetry('smus_accessProject', { + result: 'Succeeded', + }) + }) + + it('fetches all projects and switches the current project', async function () { + mockProjectNode = { + setProject: sinon.stub(), + getProject: sinon.stub().returns(mockProject), + project: mockProject, + } as any + mockDataZoneClient.fetchAllProjects.resolves([mockProject, mockProject2]) + + // Stub quickPick to return mockProject2 for the second test + const mockQuickPick = { + prompt: sinon.stub().resolves(mockProject2), + } + createQuickPickStub.restore() // Remove the previous stub + createQuickPickStub = sinon.stub(pickerPrompter, 'createQuickPick').returns(mockQuickPick as any) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.strictEqual(result, mockProject2) + assert.ok(mockDataZoneClient.fetchAllProjects.calledOnce) + assert.ok(mockDataZoneClient.fetchAllProjects.calledWith()) + assert.ok(createQuickPickStub.calledOnce) + assert.ok(mockProjectNode.setProject.calledOnce) + assert.ok(executeCommandStub.calledWith('aws.smus.rootView.refresh')) + assertTelemetry('smus_accessProject', { + result: 'Succeeded', + smusProjectId: mockProject2.id, + }) + }) + + it('shows message when no projects found', async function () { + mockDataZoneClient.fetchAllProjects.resolves([]) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.strictEqual(result, undefined) + assert.ok(!mockProjectNode.setProject.called) + }) + + it('handles API errors gracefully', async function () { + const error = new Error('API error') + mockDataZoneClient.fetchAllProjects.rejects(error) + + const result = await selectSMUSProject(mockProjectNode as any) + assert.strictEqual(result, undefined) + + assert.ok(!mockProjectNode.setProject.called) + assertTelemetry('smus_accessProject', { + result: 'Succeeded', + }) + }) + + it('handles case when user cancels project selection', async function () { + mockDataZoneClient.fetchAllProjects.resolves([mockProject, mockProject2]) + + // Make quickPick return undefined (user cancelled) + const mockQuickPick = { + prompt: sinon.stub().resolves(undefined), + } + createQuickPickStub.returns(mockQuickPick as any) + + const result = await selectSMUSProject(mockProjectNode as any) + + // Should return undefined + assert.strictEqual(result, undefined) + + // Verify project was not set + assert.ok(!mockProjectNode.setProject.called) + + // Verify refresh command was not called + assert.ok(!executeCommandStub.called) + }) + + it('handles empty projects list correctly', async function () { + mockDataZoneClient.fetchAllProjects.resolves([]) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.strictEqual(result, undefined) + assert.ok(mockDataZoneClient.fetchAllProjects.calledOnce) + assert.ok(!mockProjectNode.setProject.called) + assert.ok(!executeCommandStub.called) + }) +}) + +describe('selectSMUSProject - Additional Tests', function () { + let mockDataZoneClient: sinon.SinonStubbedInstance + let mockProjectNode: sinon.SinonStubbedInstance + let createQuickPickStub: sinon.SinonStub + let executeCommandStub: sinon.SinonStub + let getContextStub: sinon.SinonStub + let createDZClientStub: sinon.SinonStub + + const testDomainId = 'test-domain-123' + const mockProject: DataZoneProject = { + id: 'project-123', + name: 'Test Project', + description: 'Test Description', + domainId: testDomainId, + updatedAt: new Date(), + } + + beforeEach(function () { + mockDataZoneClient = { + getDomainId: sinon.stub().returns(testDomainId), + fetchAllProjects: sinon.stub(), + } as any + + mockProjectNode = { + setProject: sinon.stub(), + } as any + + // Stub createDZClientBaseOnDomainMode to return our mock client + createDZClientStub = sinon.stub() + createDZClientStub.resolves(mockDataZoneClient) + sinon.replace(utils, 'createDZClientBaseOnDomainMode', createDZClientStub) + sinon.stub(SmusAuthenticationProvider, 'fromContext').returns({ + activeConnection: { domainId: testDomainId, ssoRegion: 'us-west-2' }, + getDomainAccountId: sinon.stub().resolves('123456789012'), + getDomainId: sinon.stub().returns(testDomainId), + getDomainRegion: sinon.stub().returns('us-west-2'), + getDerCredentialsProvider: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + } as any) + + // Stub getContext to return false for IAM mode by default (non-IAM mode) + getContextStub = sinon.stub() + getContextStub.withArgs('aws.smus.isIamMode').returns(false) + getContextStub.callThrough() + sinon.replace(setContextModule, 'getContext', getContextStub) + + const mockQuickPick = { + prompt: sinon.stub().resolves(mockProject), + } + createQuickPickStub = sinon.stub(pickerPrompter, 'createQuickPick').returns(mockQuickPick as any) + executeCommandStub = sinon.stub(vscode.commands, 'executeCommand') + }) + + afterEach(function () { + sinon.restore() + }) + + it('handles access denied error gracefully', async function () { + const accessDeniedError = new Error('Access denied') + accessDeniedError.name = 'AccessDeniedError' + mockDataZoneClient.fetchAllProjects.rejects(accessDeniedError) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.strictEqual(result, undefined) + assert.ok( + createQuickPickStub.calledWith([ + { + label: '$(error)', + description: "You don't have permissions to view projects. Please contact your administrator", + }, + ]) + ) + }) + + it('shows "No projects found" message when projects list is empty', async function () { + mockDataZoneClient.fetchAllProjects.resolves([]) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.strictEqual(result, undefined) + const testWindow = getTestWindow() + assert.ok(testWindow.shownMessages.some((msg) => msg.message === 'No projects found in the domain')) + // When no projects are found, createQuickPick should not be called + assert.ok(!createQuickPickStub.called) + }) + + it('handles invalid selected project object', async function () { + mockDataZoneClient.fetchAllProjects.resolves([mockProject]) + + // Mock quickPick to return an object with 'type' property (invalid selection) + const mockQuickPick = { + prompt: sinon.stub().resolves({ type: 'invalid', data: mockProject }), + } + createQuickPickStub.returns(mockQuickPick as any) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.deepStrictEqual(result, { type: 'invalid', data: mockProject }) + assert.ok(!mockProjectNode.setProject.called) + assert.ok(!executeCommandStub.called) + }) +}) + +describe('selectSMUSProject - Express Mode', function () { + let mockDataZoneClient: sinon.SinonStubbedInstance + let mockProjectNode: sinon.SinonStubbedInstance + let createQuickPickStub: sinon.SinonStub + let executeCommandStub: sinon.SinonStub + let getContextStub: sinon.SinonStub + let getInstanceStub: sinon.SinonStub + let createDZClientStub: sinon.SinonStub + + const testDomainId = 'test-domain-123' + const testUserProfileId = 'user-profile-123' + + const userProject: DataZoneProject = { + id: 'project-123', + name: 'User Project', + description: 'Project created by user', + domainId: testDomainId, + createdBy: testUserProfileId, + updatedAt: new Date(), + } + + const otherUserProject: DataZoneProject = { + id: 'project-456', + name: 'Other User Project', + description: 'Project created by another user', + domainId: testDomainId, + createdBy: 'other-user-profile-456', + updatedAt: new Date(Date.now() - 86400000), + } + + beforeEach(function () { + const mockGroupProfileId = 'group-profile-123' + + mockDataZoneClient = { + getDomainId: sinon.stub().returns(testDomainId), + fetchAllProjects: sinon.stub(), + } as any + + mockProjectNode = { + setProject: sinon.stub(), + } as any + + // Stub createDZClientBaseOnDomainMode to return our mock client + createDZClientStub = sinon.stub() + createDZClientStub.resolves(mockDataZoneClient) + sinon.replace(utils, 'createDZClientBaseOnDomainMode', createDZClientStub) + + // Mock credentials provider + const mockCredentialsProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + + const mockAuthProvider = { + activeConnection: { + type: 'iam' as const, + profileName: 'test-profile', + region: 'us-west-2', + domainId: testDomainId, + domainUrl: `https://${testDomainId}.sagemaker.us-west-2.on.aws/`, + }, + getDomainAccountId: sinon.stub().resolves('123456789012'), + getDomainId: sinon.stub().returns(testDomainId), + getDomainRegion: sinon.stub().returns('us-west-2'), + getCredentialsProviderForIamProfile: sinon.stub().resolves(mockCredentialsProvider), + getIamPrincipalArn: sinon.stub().resolves('arn:aws:sts::123456789012:assumed-role/TestRole/test-session'), + } + sinon.stub(SmusAuthenticationProvider, 'fromContext').returns(mockAuthProvider as any) + + // Mock DataZoneCustomClientHelper + const mockDataZoneCustomClientHelper = { + getGroupProfileId: sinon.stub().resolves(mockGroupProfileId), + } + getInstanceStub = sinon + .stub(DataZoneCustomClientHelper, 'getInstance') + .returns(mockDataZoneCustomClientHelper as any) + + // Mock STS client + sinon.stub(DefaultStsClient.prototype, 'getCallerIdentity').resolves({ + Arn: 'arn:aws:sts::123456789012:assumed-role/TestRole/test-session', + UserId: 'AIDAI123456789EXAMPLE:test-session', + Account: '123456789012', + }) + + // Mock SmusUtils - simulate IAM role session (not IAM user) + sinon.stub(SmusUtils, 'isIamUserArn').returns(false) + sinon.stub(SmusUtils, 'convertAssumedRoleArnToIamRoleArn').returns('arn:aws:iam::123456789012:role/TestRole') + + const mockQuickPick = { + prompt: sinon.stub().resolves(userProject), + } + createQuickPickStub = sinon.stub(pickerPrompter, 'createQuickPick').returns(mockQuickPick as any) + executeCommandStub = sinon.stub(vscode.commands, 'executeCommand') + + // Stub getContext to simulate IAM mode + getContextStub = sinon.stub() + getContextStub.withArgs('aws.smus.isIamMode').returns(true) + getContextStub.callThrough() + sinon.replace(setContextModule, 'getContext', getContextStub) + }) + + afterEach(function () { + sinon.restore() + }) + + it('filters projects to show only user-created projects in IAM mode', async function () { + mockDataZoneClient.fetchAllProjects.resolves([userProject, otherUserProject]) + + const result = await selectSMUSProject(mockProjectNode as any) + + // Verify DataZoneCustomClientHelper.getInstance was called + assert.ok(getInstanceStub.called) + + // Verify projects were fetched with group identifier + assert.ok(mockDataZoneClient.fetchAllProjects.calledOnce) + const fetchCallArgs = mockDataZoneClient.fetchAllProjects.getCall(0).args[0] + assert.ok(fetchCallArgs?.groupIdentifier) + + // Verify the project was selected and set + assert.strictEqual(result, userProject) + assert.ok(mockProjectNode.setProject.calledOnce) + assert.ok(executeCommandStub.calledWith('aws.smus.rootView.refresh')) + }) + + it('shows message when no user-created projects found in IAM mode', async function () { + mockDataZoneClient.fetchAllProjects.resolves([]) + + const result = await selectSMUSProject(mockProjectNode as any) + + // Verify DataZoneCustomClientHelper.getInstance was called + assert.ok(getInstanceStub.called) + + // Verify no projects were shown in quick pick + assert.ok(!createQuickPickStub.called) + + // Verify appropriate message was shown + const testWindow = getTestWindow() + assert.ok( + testWindow.shownMessages.some( + (msg) => msg.message === 'No accessible projects found for your IAM principal' + ) + ) + + // Verify no project was set + assert.strictEqual(result, undefined) + assert.ok(!mockProjectNode.setProject.called) + }) + + it('shows all user-created projects when multiple exist in IAM mode', async function () { + const userProject2: DataZoneProject = { + id: 'project-789', + name: 'Another User Project', + description: 'Another project created by user', + domainId: testDomainId, + createdBy: testUserProfileId, + updatedAt: new Date(Date.now() - 172800000), // 2 days ago + } + + // In IAM mode, fetchAllProjects is called with groupIdentifier filter + // So the API returns only projects for that group (already filtered) + mockDataZoneClient.fetchAllProjects.resolves([userProject, userProject2]) + + await selectSMUSProject(mockProjectNode as any) + + // Verify projects were fetched with group identifier + assert.ok(mockDataZoneClient.fetchAllProjects.calledOnce) + const fetchCallArgs = mockDataZoneClient.fetchAllProjects.getCall(0).args[0] + assert.ok(fetchCallArgs?.groupIdentifier) + + // Verify all returned projects are shown in quick pick + const quickPickCall = createQuickPickStub.getCall(0) + const items = quickPickCall.args[0] + assert.strictEqual(items.length, 2) + assert.ok(items.some((item: any) => item.data.id === userProject.id)) + assert.ok(items.some((item: any) => item.data.id === userProject2.id)) + }) + + it('does not filter projects in non-IAM mode', async function () { + // Stub getContext to return false for IAM mode + getContextStub.withArgs('aws.smus.isIamMode').returns(false) + + mockDataZoneClient.fetchAllProjects.resolves([userProject, otherUserProject]) + + await selectSMUSProject(mockProjectNode as any) + + // Verify DataZoneCustomClientHelper.getInstance was NOT called in non-IAM mode + assert.ok(!getInstanceStub.called) + + // Verify all projects are shown in quick pick + const quickPickCall = createQuickPickStub.getCall(0) + const items = quickPickCall.args[0] + assert.strictEqual(items.length, 2) + assert.ok(items.some((item: any) => item.data.id === userProject.id)) + assert.ok(items.some((item: any) => item.data.id === otherUserProject.id)) + }) +}) + +describe('selectSMUSProject - Error Handling', function () { + let mockDataZoneClient: sinon.SinonStubbedInstance + let mockProjectNode: sinon.SinonStubbedInstance + let createQuickPickStub: sinon.SinonStub + let getContextStub: sinon.SinonStub + let createDZClientStub: sinon.SinonStub + + const testDomainId = 'test-domain-123' + const testUserProfileId = 'user-profile-123' + + const mockProject: DataZoneProject = { + id: 'project-123', + name: 'Test Project', + description: 'Test Description', + domainId: testDomainId, + createdBy: testUserProfileId, + updatedAt: new Date(), + } + + beforeEach(function () { + mockDataZoneClient = { + getDomainId: sinon.stub().returns(testDomainId), + fetchAllProjects: sinon.stub(), + getUserProfileId: sinon.stub().resolves(testUserProfileId), + } as any + + mockProjectNode = { + setProject: sinon.stub(), + } as any + + createDZClientStub = sinon.stub() + createDZClientStub.resolves(mockDataZoneClient) + sinon.replace(utils, 'createDZClientBaseOnDomainMode', createDZClientStub) + + sinon.stub(SmusAuthenticationProvider, 'fromContext').returns({ + activeConnection: { domainId: testDomainId, ssoRegion: 'us-west-2' }, + getDomainAccountId: sinon.stub().resolves('123456789012'), + getDomainId: sinon.stub().returns(testDomainId), + getDomainRegion: sinon.stub().returns('us-west-2'), + } as any) + + getContextStub = sinon.stub() + getContextStub.withArgs('aws.smus.isIamMode').returns(false) + getContextStub.callThrough() + sinon.replace(setContextModule, 'getContext', getContextStub) + + const mockQuickPick = { + prompt: sinon.stub().resolves(mockProject), + } + createQuickPickStub = sinon.stub(pickerPrompter, 'createQuickPick').returns(mockQuickPick as any) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('No projects scenario', function () { + it('displays "No projects found in the domain" message when user has no projects', async function () { + mockDataZoneClient.fetchAllProjects.resolves([]) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.strictEqual(result, undefined) + const testWindow = getTestWindow() + assert.ok(testWindow.shownMessages.some((msg) => msg.message === 'No projects found in the domain')) + // createQuickPick should NOT be called when there are no projects + assert.ok(!createQuickPickStub.called) + assert.ok(!mockProjectNode.setProject.called) + }) + }) + + describe('No accessible projects in IAM mode', function () { + beforeEach(function () { + getContextStub.withArgs('aws.smus.isIamMode').returns(true) + + // Override the SSO connection with IAM connection for IAM mode tests + sinon.restore() + + // Re-setup mocks with IAM connection + mockDataZoneClient = { + getDomainId: sinon.stub().returns(testDomainId), + fetchAllProjects: sinon.stub(), + getUserProfileId: sinon.stub().resolves(testUserProfileId), + } as any + + mockProjectNode = { + setProject: sinon.stub(), + } as any + + createDZClientStub = sinon.stub() + createDZClientStub.resolves(mockDataZoneClient) + sinon.replace(utils, 'createDZClientBaseOnDomainMode', createDZClientStub) + + // Mock credentials provider + const mockCredentialsProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + + const mockAuthProvider = { + activeConnection: { + type: 'iam' as const, + profileName: 'test-profile', + region: 'us-west-2', + domainId: testDomainId, + domainUrl: `https://${testDomainId}.sagemaker.us-west-2.on.aws/`, + }, + getDomainAccountId: sinon.stub().resolves('123456789012'), + getDomainId: sinon.stub().returns(testDomainId), + getDomainRegion: sinon.stub().returns('us-west-2'), + getCredentialsProviderForIamProfile: sinon.stub().resolves(mockCredentialsProvider), + getIamPrincipalArn: sinon + .stub() + .resolves('arn:aws:sts::123456789012:assumed-role/TestRole/test-session'), + } + sinon.stub(SmusAuthenticationProvider, 'fromContext').returns(mockAuthProvider as any) + + // Mock DataZoneCustomClientHelper + const mockDataZoneCustomClientHelper = { + getGroupProfileId: sinon.stub().resolves('group-profile-123'), + } + sinon.stub(DataZoneCustomClientHelper, 'getInstance').returns(mockDataZoneCustomClientHelper as any) + + // Mock STS client + sinon.stub(DefaultStsClient.prototype, 'getCallerIdentity').resolves({ + Arn: 'arn:aws:sts::123456789012:assumed-role/TestRole/test-session', + UserId: 'AIDAI123456789EXAMPLE:test-session', + Account: '123456789012', + }) + + // Mock SmusUtils - simulate IAM role session (not IAM user) + sinon.stub(SmusUtils, 'isIamUserArn').returns(false) + sinon + .stub(SmusUtils, 'convertAssumedRoleArnToIamRoleArn') + .returns('arn:aws:iam::123456789012:role/TestRole') + + const mockQuickPick = { + prompt: sinon.stub().resolves(mockProject), + } + createQuickPickStub = sinon.stub(pickerPrompter, 'createQuickPick').returns(mockQuickPick as any) + + getContextStub = sinon.stub() + getContextStub.withArgs('aws.smus.isIamMode').returns(true) + getContextStub.callThrough() + sinon.replace(setContextModule, 'getContext', getContextStub) + }) + + it('displays "No accessible projects found" when user has no projects they created', async function () { + // In IAM mode, fetchAllProjects is called with groupIdentifier filter + // which should return empty array when no projects match + mockDataZoneClient.fetchAllProjects.resolves([]) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.strictEqual(result, undefined) + const testWindow = getTestWindow() + assert.ok( + testWindow.shownMessages.some( + (msg) => msg.message === 'No accessible projects found for your IAM principal' + ) + ) + assert.ok(!mockProjectNode.setProject.called) + }) + + it('handles getGroupProfileId failure with appropriate error message', async function () { + const testRoleArn = 'arn:aws:iam::123456789012:role/TestRole' + + // Mock getGroupProfileId to throw a ToolkitError with NoGroupProfileFound code + const groupProfileError = new ToolkitError(`No group profile found for IAM role: ${testRoleArn}`, { + code: SmusErrorCodes.NoGroupProfileFound, + name: 'ToolkitError', + }) + const mockDataZoneCustomClientHelper = { + getGroupProfileId: sinon.stub().rejects(groupProfileError), + } + sinon.restore() + sinon.stub(DataZoneCustomClientHelper, 'getInstance').returns(mockDataZoneCustomClientHelper as any) + + // Re-stub other dependencies + const mockAuthProvider = { + activeConnection: { + type: 'iam' as const, + profileName: 'test-profile', + region: 'us-west-2', + domainId: testDomainId, + domainUrl: `https://${testDomainId}.sagemaker.us-west-2.on.aws/`, + }, + getDomainAccountId: sinon.stub().resolves('123456789012'), + getDomainId: sinon.stub().returns(testDomainId), + getDomainRegion: sinon.stub().returns('us-west-2'), + getCredentialsProviderForIamProfile: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + getIamPrincipalArn: sinon.stub().resolves(testRoleArn), + } + sinon.stub(SmusAuthenticationProvider, 'fromContext').returns(mockAuthProvider as any) + + sinon.stub(DefaultStsClient.prototype, 'getCallerIdentity').resolves({ + Arn: 'arn:aws:sts::123456789012:assumed-role/TestRole/test-session', + UserId: 'AIDAI123456789EXAMPLE:test-session', + Account: '123456789012', + }) + + const getContextStub = sinon.stub() + getContextStub.withArgs('aws.smus.isIamMode').returns(true) + getContextStub.callThrough() + sinon.replace(setContextModule, 'getContext', getContextStub) + + sinon.replace(utils, 'createDZClientBaseOnDomainMode', sinon.stub().resolves(mockDataZoneClient)) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.ok(result instanceof Error) + const testWindow = getTestWindow() + assert.ok( + testWindow.shownMessages.some((msg) => + msg.message.includes(`No resources found for IAM principal: ${testRoleArn}`) + ) + ) + }) + + it('handles getUserProfileIdForIamPrincipal failure with appropriate error message', async function () { + const testUserArn = 'arn:aws:iam::123456789012:user/test-user' + + // Mock getUserProfileIdForIamPrincipal to throw a ToolkitError with NoUserProfileFound code + const userProfileError = new ToolkitError(`No user profile found for IAM principal: ${testUserArn}`, { + code: SmusErrorCodes.NoUserProfileFound, + name: 'ToolkitError', + }) + + sinon.restore() + + // Re-stub dependencies for IAM user flow + const mockAuthProvider = { + activeConnection: { + type: 'iam' as const, + profileName: 'test-profile', + region: 'us-west-2', + domainId: testDomainId, + domainUrl: `https://${testDomainId}.sagemaker.us-west-2.on.aws/`, + }, + getDomainAccountId: sinon.stub().resolves('123456789012'), + getDomainId: sinon.stub().returns(testDomainId), + getDomainRegion: sinon.stub().returns('us-west-2'), + getCredentialsProviderForIamProfile: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + getIamPrincipalArn: sinon.stub().resolves(testUserArn), + } + sinon.stub(SmusAuthenticationProvider, 'fromContext').returns(mockAuthProvider as any) + + // Mock SmusUtils to indicate this is an IAM user + sinon.stub(SmusUtils, 'isIamUserArn').returns(true) + + const getContextStub = sinon.stub() + getContextStub.withArgs('aws.smus.isIamMode').returns(true) + getContextStub.callThrough() + sinon.replace(setContextModule, 'getContext', getContextStub) + + // Create a new mock client with the failing method + const failingMockDataZoneClient = { + getDomainId: sinon.stub().returns(testDomainId), + fetchAllProjects: sinon.stub(), + getUserProfileIdForIamPrincipal: sinon.stub().rejects(userProfileError), + } as any + + sinon.replace(utils, 'createDZClientBaseOnDomainMode', sinon.stub().resolves(failingMockDataZoneClient)) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.ok(result instanceof Error) + const testWindow = getTestWindow() + assert.ok( + testWindow.shownMessages.some((msg) => + msg.message.includes(`No resources found for IAM principal: ${testUserArn}`) + ) + ) + }) + }) + + describe('Access denied scenarios', function () { + it('displays appropriate error message when user lacks permissions to view projects', async function () { + const accessDeniedError = new Error('Access denied to list projects') + accessDeniedError.name = 'AccessDeniedException' + mockDataZoneClient.fetchAllProjects.rejects(accessDeniedError) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.strictEqual(result, undefined) + assert.ok( + createQuickPickStub.calledWith([ + { + label: '$(error)', + description: "You don't have permissions to view projects. Please contact your administrator", + }, + ]) + ) + assert.ok(!mockProjectNode.setProject.called) + }) + + it('handles AccessDenied error name variations', async function () { + const accessDeniedError = new Error('Access denied') + accessDeniedError.name = 'AccessDeniedError' + mockDataZoneClient.fetchAllProjects.rejects(accessDeniedError) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.strictEqual(result, undefined) + assert.ok( + createQuickPickStub.calledWith([ + { + label: '$(error)', + description: "You don't have permissions to view projects. Please contact your administrator", + }, + ]) + ) + }) + + it('handles UnauthorizedOperation error as access denied', async function () { + const unauthorizedError = new Error('Unauthorized operation') + unauthorizedError.name = 'UnauthorizedOperationAccessDenied' + mockDataZoneClient.fetchAllProjects.rejects(unauthorizedError) + + const result = await selectSMUSProject(mockProjectNode as any) + + assert.strictEqual(result, undefined) + assert.ok( + createQuickPickStub.calledWith([ + { + label: '$(error)', + description: "You don't have permissions to view projects. Please contact your administrator", + }, + ]) + ) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode.test.ts new file mode 100644 index 00000000000..a44b2ec3e7d --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode.test.ts @@ -0,0 +1,280 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import * as vscode from 'vscode' +import { SagemakerUnifiedStudioSpaceNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode' +import { SageMakerUnifiedStudioSpacesParentNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode' +import { SagemakerClient, SagemakerSpaceApp } from '../../../../shared/clients/sagemaker' +import { SagemakerSpace } from '../../../../awsService/sagemaker/sagemakerSpace' + +describe('SagemakerUnifiedStudioSpaceNode', function () { + let spaceNode: SagemakerUnifiedStudioSpaceNode + let mockParent: SageMakerUnifiedStudioSpacesParentNode + let mockSagemakerClient: SagemakerClient + let mockSpaceApp: SagemakerSpaceApp + let mockSagemakerSpace: sinon.SinonStubbedInstance + let trackPendingNodeStub: sinon.SinonStub + + beforeEach(function () { + trackPendingNodeStub = sinon.stub() + mockParent = { + trackPendingNode: trackPendingNodeStub, + } as any + + mockSagemakerClient = { + describeApp: sinon.stub(), + describeSpace: sinon.stub(), + } as any + + mockSpaceApp = { + SpaceName: 'test-space', + DomainId: 'domain-123', + Status: 'InService', + DomainSpaceKey: 'domain-123:test-space', + App: { + AppName: 'test-app', + Status: 'InService', + }, + } as any + + mockSagemakerSpace = { + label: 'test-space (Running)', + description: 'Private space', + tooltip: new vscode.MarkdownString('Space tooltip'), + iconPath: { light: 'light-icon', dark: 'dark-icon' }, + contextValue: 'smusSpaceNode', + updateSpace: sinon.stub(), + setSpaceStatus: sinon.stub(), + isPending: sinon.stub().returns(false), + getStatus: sinon.stub().returns('Running'), + getAppStatus: sinon.stub().resolves('InService'), + name: 'test-space', + arn: 'arn:aws:sagemaker:us-west-2:123456789012:space/test-space', + getAppArn: sinon.stub().resolves('arn:aws:sagemaker:us-west-2:123456789012:app/test-app'), + getSpaceArn: sinon.stub().resolves('arn:aws:sagemaker:us-west-2:123456789012:space/test-space'), + updateSpaceAppStatus: sinon.stub().resolves(), + buildTooltip: sinon.stub().returns('Space tooltip'), + getAppIcon: sinon.stub().returns({ light: 'light-icon', dark: 'dark-icon' }), + DomainSpaceKey: 'domain-123:test-space', + } as any + + sinon.stub(SagemakerSpace.prototype, 'constructor' as any).returns(mockSagemakerSpace) + + spaceNode = new SagemakerUnifiedStudioSpaceNode( + mockParent, + mockSagemakerClient, + 'us-west-2', + mockSpaceApp, + true + ) + + // Replace the internal smSpace with our mock + ;(spaceNode as any).smSpace = mockSagemakerSpace + }) + + afterEach(function () { + sinon.restore() + }) + + describe('constructor', function () { + it('creates instance with correct properties', function () { + assert.strictEqual(spaceNode.id, 'smusSpaceNodetest-space') + assert.strictEqual(spaceNode.resource, spaceNode) + assert.strictEqual(spaceNode.regionCode, 'us-west-2') + assert.strictEqual(spaceNode.spaceApp, mockSpaceApp) + }) + }) + + describe('getTreeItem', function () { + it('returns correct tree item', function () { + const treeItem = spaceNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'test-space (Running)') + assert.strictEqual(treeItem.description, 'Private space') + assert.strictEqual(treeItem.contextValue, 'smusSpaceNode') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.ok(treeItem.iconPath) + assert.ok(treeItem.tooltip) + }) + }) + + describe('getChildren', function () { + it('returns empty array', function () { + const children = spaceNode.getChildren() + assert.deepStrictEqual(children, []) + }) + }) + + describe('getParent', function () { + it('returns parent node', function () { + const parent = spaceNode.getParent() + assert.strictEqual(parent, mockParent) + }) + }) + + describe('refreshNode', function () { + it('fires change event', async function () { + const emitterSpy = sinon.spy(spaceNode['onDidChangeEmitter'], 'fire') + await spaceNode.refreshNode() + assert(emitterSpy.calledOnce) + }) + }) + + describe('updateSpace', function () { + it('updates space and tracks pending node when pending', function () { + mockSagemakerSpace.isPending.returns(true) + const newSpaceApp = { ...mockSpaceApp, Status: 'Pending' } + + spaceNode.updateSpace(newSpaceApp) + + assert(mockSagemakerSpace.updateSpace.calledWith(newSpaceApp)) + assert(trackPendingNodeStub.calledWith('domain-123:test-space')) + }) + + it('updates space without tracking when not pending', function () { + mockSagemakerSpace.isPending.returns(false) + const newSpaceApp = { ...mockSpaceApp, Status: 'InService' } + + spaceNode.updateSpace(newSpaceApp) + + assert(mockSagemakerSpace.updateSpace.calledWith(newSpaceApp)) + assert(trackPendingNodeStub.notCalled) + }) + }) + + describe('setSpaceStatus', function () { + it('delegates to SagemakerSpace', function () { + spaceNode.setSpaceStatus('InService', 'Running') + assert(mockSagemakerSpace.setSpaceStatus.calledWith('InService', 'Running')) + }) + }) + + describe('isPending', function () { + it('delegates to SagemakerSpace', function () { + const result = spaceNode.isPending() + assert(mockSagemakerSpace.isPending.called) + assert.strictEqual(result, false) + }) + }) + + describe('getStatus', function () { + it('delegates to SagemakerSpace', function () { + const result = spaceNode.getStatus() + assert(mockSagemakerSpace.getStatus.called) + assert.strictEqual(result, 'Running') + }) + }) + + describe('getAppStatus', function () { + it('delegates to SagemakerSpace', async function () { + const result = await spaceNode.getAppStatus() + assert(mockSagemakerSpace.getAppStatus.called) + assert.strictEqual(result, 'InService') + }) + }) + + describe('name property', function () { + it('returns space name', function () { + assert.strictEqual(spaceNode.name, 'test-space') + }) + }) + + describe('arn property', function () { + it('returns space arn', function () { + assert.strictEqual(spaceNode.arn, 'arn:aws:sagemaker:us-west-2:123456789012:space/test-space') + }) + }) + + describe('getAppArn', function () { + it('delegates to SagemakerSpace', async function () { + const result = await spaceNode.getAppArn() + assert(mockSagemakerSpace.getAppArn.called) + assert.strictEqual(result, 'arn:aws:sagemaker:us-west-2:123456789012:app/test-app') + }) + }) + + describe('getSpaceArn', function () { + it('delegates to SagemakerSpace', async function () { + const result = await spaceNode.getSpaceArn() + assert(mockSagemakerSpace.getSpaceArn.called) + assert.strictEqual(result, 'arn:aws:sagemaker:us-west-2:123456789012:space/test-space') + }) + }) + + describe('updateSpaceAppStatus', function () { + it('updates status and tracks pending node when pending', async function () { + mockSagemakerSpace.isPending.returns(true) + + await spaceNode.updateSpaceAppStatus() + + assert(mockSagemakerSpace.updateSpaceAppStatus.called) + assert(trackPendingNodeStub.calledWith('domain-123:test-space')) + }) + + it('updates status without tracking when not pending', async function () { + mockSagemakerSpace.isPending.returns(false) + + await spaceNode.updateSpaceAppStatus() + + assert(mockSagemakerSpace.updateSpaceAppStatus.called) + assert(trackPendingNodeStub.notCalled) + }) + }) + + describe('buildTooltip', function () { + it('delegates to SagemakerSpace', function () { + const result = spaceNode.buildTooltip() + assert(mockSagemakerSpace.buildTooltip.called) + assert.strictEqual(result, 'Space tooltip') + }) + }) + + describe('getAppIcon', function () { + it('delegates to SagemakerSpace', function () { + const result = spaceNode.getAppIcon() + assert(mockSagemakerSpace.getAppIcon.called) + assert.deepStrictEqual(result, { light: 'light-icon', dark: 'dark-icon' }) + }) + }) + + describe('DomainSpaceKey property', function () { + it('returns domain space key', function () { + assert.strictEqual(spaceNode.DomainSpaceKey, 'domain-123:test-space') + }) + }) + + describe('SagemakerSpace getContext for SMUS', function () { + it('returns awsSagemakerSpaceRunningNode for running SMUS space with undefined RemoteAccess', function () { + // Create a space app without RemoteAccess setting (undefined) + const smusSpaceApp = { + SpaceName: 'test-space', + DomainId: 'domain-123', + Status: 'InService', + DomainSpaceKey: 'domain-123:test-space', + App: { + AppName: 'test-app', + Status: 'InService', + }, + SpaceSettingsSummary: { + // RemoteAccess is undefined + }, + } as any + + // Create a real SagemakerSpace instance for SMUS to test the actual getContext logic + const realSagemakerSpace = new SagemakerSpace( + mockSagemakerClient, + 'us-west-2', + smusSpaceApp, + true // isSMUSSpace = true + ) + + const context = realSagemakerSpace.getContext() + + assert.strictEqual(context, 'awsSagemakerSpaceRunningNode') + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode.test.ts new file mode 100644 index 00000000000..6ac3d7e0147 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode.test.ts @@ -0,0 +1,618 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import sinon from 'sinon' +import * as vscode from 'vscode' +import { SageMakerUnifiedStudioSpacesParentNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode' +import { SageMakerUnifiedStudioComputeNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode' +import { SagemakerUnifiedStudioSpaceNode } from '../../../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode' +import { DataZoneClient } from '../../../../sagemakerunifiedstudio/shared/client/datazoneClient' +import { SagemakerClient } from '../../../../shared/clients/sagemaker' +import { SmusAuthenticationProvider } from '../../../../sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider' +import { getLogger } from '../../../../shared/logger/logger' +import { SmusUtils, SmusErrorCodes } from '../../../../sagemakerunifiedstudio/shared/smusUtils' +import { ToolkitError } from '../../../../shared/errors' +import * as vscodeUtils from '../../../../shared/vscode/setContext' +import * as utils from '../../../../sagemakerunifiedstudio/explorer/nodes/utils' +import { DataZoneCustomClientHelper } from '../../../../sagemakerunifiedstudio/shared/client/datazoneCustomClientHelper' + +describe('SageMakerUnifiedStudioSpacesParentNode', function () { + let spacesNode: SageMakerUnifiedStudioSpacesParentNode + let mockParent: SageMakerUnifiedStudioComputeNode + let mockExtensionContext: vscode.ExtensionContext + let mockAuthProvider: SmusAuthenticationProvider + let mockSagemakerClient: sinon.SinonStubbedInstance + let mockDataZoneClient: sinon.SinonStubbedInstance + + beforeEach(function () { + mockParent = {} as any + mockExtensionContext = { + extensionUri: vscode.Uri.file('/test'), + } as any + mockAuthProvider = { + activeConnection: { domainId: 'test-domain', ssoRegion: 'us-west-2', profileName: 'test-profile' }, + getDomainId: sinon.stub().returns('test-domain'), + getDomainRegion: sinon.stub().returns('us-west-2'), + getIamPrincipalArn: sinon.stub().resolves(undefined), + getDerCredentialsProvider: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + getCredentialsProviderForIamProfile: sinon.stub().resolves({ + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + }), + } as any + mockSagemakerClient = sinon.createStubInstance(SagemakerClient) + mockSagemakerClient.fetchSpaceAppsAndDomains.resolves([new Map(), new Map()]) + + mockDataZoneClient = { + getInstance: sinon.stub(), + getUserId: sinon.stub(), + getDomainId: sinon.stub(), + getRegion: sinon.stub(), + getToolingEnvironmentId: sinon.stub(), + getEnvironmentDetails: sinon.stub(), + getToolingEnvironment: sinon.stub(), + } as any + + sinon.stub(DataZoneClient, 'createWithCredentials').resolves(mockDataZoneClient as any) + sinon.stub(getLogger(), 'debug') + sinon.stub(getLogger(), 'error') + sinon.stub(SmusUtils, 'extractSSOIdFromUserId').returns('user-12345') + sinon.stub(vscodeUtils, 'getContext').returns(false) + + spacesNode = new SageMakerUnifiedStudioSpacesParentNode( + mockParent, + 'project-123', + mockExtensionContext, + mockAuthProvider, + mockSagemakerClient as any + ) + }) + + afterEach(function () { + spacesNode.pollingSet.clear() + sinon.restore() + }) + + describe('constructor', function () { + it('creates instance with correct properties', function () { + assert.strictEqual(spacesNode.id, 'smusSpacesParentNode') + assert.strictEqual(spacesNode.resource, spacesNode) + }) + }) + + describe('getTreeItem', function () { + it('returns correct tree item', async function () { + const treeItem = await spacesNode.getTreeItem() + + assert.strictEqual(treeItem.label, 'Spaces') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Expanded) + assert.strictEqual(treeItem.contextValue, 'smusSpacesNode') + assert.ok(treeItem.iconPath) + }) + }) + + describe('getParent', function () { + it('returns parent node', function () { + const parent = spacesNode.getParent() + assert.strictEqual(parent, mockParent) + }) + }) + + describe('getProjectId', function () { + it('returns project ID', function () { + assert.strictEqual(spacesNode.getProjectId(), 'project-123') + }) + }) + + describe('getAuthProvider', function () { + it('returns auth provider', function () { + assert.strictEqual(spacesNode.getAuthProvider(), mockAuthProvider) + }) + }) + + describe('refreshNode', function () { + it('fires change event', async function () { + const emitterSpy = sinon.spy(spacesNode['onDidChangeEmitter'], 'fire') + await spacesNode.refreshNode() + assert(emitterSpy.calledOnce) + }) + }) + + describe('trackPendingNode', function () { + it('adds node to polling set', function () { + const addSpy = sinon.spy(spacesNode.pollingSet, 'add') + spacesNode.trackPendingNode('test-key') + assert(addSpy.calledWith('test-key')) + }) + }) + + describe('getSpaceNodes', function () { + it('returns space node when found', function () { + const mockSpaceNode = {} as SagemakerUnifiedStudioSpaceNode + spacesNode['sagemakerSpaceNodes'].set('test-key', mockSpaceNode) + + const result = spacesNode.getSpaceNodes('test-key') + assert.strictEqual(result, mockSpaceNode) + }) + + it('throws error when node not found', function () { + assert.throws( + () => spacesNode.getSpaceNodes('non-existent'), + /Node with id non-existent from polling set not found/ + ) + }) + }) + + describe('getSageMakerDomainId', function () { + it('throws error when no active connection', async function () { + const mockAuthProviderNoConnection = { + activeConnection: undefined, + } as any + + const spacesNodeNoConnection = new SageMakerUnifiedStudioSpacesParentNode( + mockParent, + 'project-123', + mockExtensionContext, + mockAuthProviderNoConnection, + mockSagemakerClient as any + ) + + await assert.rejects( + async () => await spacesNodeNoConnection.getSageMakerDomainId(), + /No active connection found to get SageMaker domain ID/ + ) + }) + + it('throws error when DataZone client not initialized', async function () { + ;(DataZoneClient.createWithCredentials as sinon.SinonStub).resolves(undefined) + + await assert.rejects( + async () => await spacesNode.getSageMakerDomainId(), + /DataZone client is not initialized/ + ) + }) + + it('throws error when tooling environment ID not found', async function () { + mockDataZoneClient.getDomainId.returns('domain-123') + const error = new Error('Failed to get tooling environment ID: Environment not found') + mockDataZoneClient.getToolingEnvironment.rejects(error) + + await assert.rejects( + async () => await spacesNode.getSageMakerDomainId(), + /Failed to get tooling environment ID: Environment not found/ + ) + }) + + it('throws error when no default environment found', async function () { + mockDataZoneClient.getDomainId.returns('domain-123') + const error = new Error('No default environment found for project') + mockDataZoneClient.getToolingEnvironment.rejects(error) + + await assert.rejects( + async () => await spacesNode.getSageMakerDomainId(), + /No default environment found for project/ + ) + }) + + it('throws error when SageMaker domain ID not found in resources', async function () { + mockDataZoneClient.getDomainId.returns('domain-123') + mockDataZoneClient.getToolingEnvironment.resolves({ + projectId: 'project-123', + domainId: 'domain-123', + createdBy: 'user', + name: 'test-env', + awsAccountRegion: 'us-west-2', + provisionedResources: [{ name: 'otherResource', value: 'value', type: 'OTHER' }], + } as any) + + await assert.rejects( + async () => await spacesNode.getSageMakerDomainId(), + /No SageMaker domain found in the tooling environment/ + ) + }) + + it('returns SageMaker domain ID when found', async function () { + mockDataZoneClient.getDomainId.returns('domain-123') + mockDataZoneClient.getToolingEnvironment.resolves({ + projectId: 'project-123', + domainId: 'domain-123', + createdBy: 'user', + name: 'test-env', + awsAccountRegion: 'us-west-2', + provisionedResources: [ + { + name: 'sageMakerDomainId', + value: 'sagemaker-domain-123', + type: 'SAGEMAKER_DOMAIN', + }, + ], + } as any) + + const result = await spacesNode.getSageMakerDomainId() + assert.strictEqual(result, 'sagemaker-domain-123') + }) + }) + + describe('getChildren', function () { + let updateChildrenStub: sinon.SinonStub + let mockSpaceNode1: SagemakerUnifiedStudioSpaceNode + let mockSpaceNode2: SagemakerUnifiedStudioSpaceNode + + beforeEach(function () { + updateChildrenStub = sinon.stub(spacesNode as any, 'updateChildren').resolves() + mockSpaceNode1 = { id: 'space1' } as any + mockSpaceNode2 = { id: 'space2' } as any + }) + + it('returns space nodes when spaces exist', async function () { + spacesNode['sagemakerSpaceNodes'].set('space1', mockSpaceNode1) + spacesNode['sagemakerSpaceNodes'].set('space2', mockSpaceNode2) + + const children = await spacesNode.getChildren() + + assert.strictEqual(children.length, 2) + assert(children.includes(mockSpaceNode1)) + assert(children.includes(mockSpaceNode2)) + assert(updateChildrenStub.calledOnce) + }) + + it('returns no spaces found node when no spaces exist', async function () { + const children = await spacesNode.getChildren() + + assert.strictEqual(children.length, 1) + const noSpacesNode = children[0] + assert.strictEqual(noSpacesNode.id, 'smusNoSpaces') + + const treeItem = await noSpacesNode.getTreeItem() + assert.strictEqual(treeItem.label, '[No Spaces found]') + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + }) + + it('returns no spaces found node when updateChildren throws error', async function () { + updateChildrenStub.rejects(new Error('Update failed')) + + const children = await spacesNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].id, 'smusNoSpaces') + }) + + it('returns access denied node when AccessDeniedException is thrown', async function () { + const accessDeniedError = new Error('Access denied') + accessDeniedError.name = 'AccessDeniedException' + updateChildrenStub.rejects(accessDeniedError) + + const children = await spacesNode.getChildren() + + assert.strictEqual(children.length, 1) + const accessDeniedNode = children[0] + assert.strictEqual(accessDeniedNode.id, 'smusAccessDenied') + + const treeItem = await accessDeniedNode.getTreeItem() + assert.ok(treeItem) + assert.strictEqual( + treeItem.label, + "You don't have permission to view spaces. Please contact your administrator." + ) + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.ok(treeItem.iconPath) + assert.strictEqual((treeItem.iconPath as vscode.ThemeIcon).id, 'error') + }) + }) + + describe('updatePendingNodes', function () { + it('updates pending space nodes and removes from polling set when not pending', async function () { + const mockSpaceNode = { + DomainSpaceKey: 'test-key', + updateSpaceAppStatus: sinon.stub().resolves(), + isPending: sinon.stub().returns(false), + refreshNode: sinon.stub().resolves(), + } as any + + spacesNode['sagemakerSpaceNodes'].set('test-key', mockSpaceNode) + spacesNode.pollingSet.add('test-key') + + await spacesNode['updatePendingNodes']() + + assert(mockSpaceNode.updateSpaceAppStatus.calledOnce) + assert(mockSpaceNode.refreshNode.calledOnce) + assert(!spacesNode.pollingSet.has('test-key')) + }) + + it('keeps pending nodes in polling set', async function () { + const mockSpaceNode = { + DomainSpaceKey: 'test-key', + updateSpaceAppStatus: sinon.stub().resolves(), + isPending: sinon.stub().returns(true), + refreshNode: sinon.stub().resolves(), + } as any + + spacesNode['sagemakerSpaceNodes'].set('test-key', mockSpaceNode) + spacesNode.pollingSet.add('test-key') + + await spacesNode['updatePendingNodes']() + + assert(mockSpaceNode.updateSpaceAppStatus.calledOnce) + assert(mockSpaceNode.refreshNode.notCalled) + assert(spacesNode.pollingSet.has('test-key')) + }) + }) + + describe('getAccessDeniedChildren', function () { + it('returns access denied tree node with error icon', async function () { + const accessDeniedChildren = spacesNode['getAccessDeniedChildren']() + + assert.strictEqual(accessDeniedChildren.length, 1) + const accessDeniedNode = accessDeniedChildren[0] + assert.strictEqual(accessDeniedNode.id, 'smusAccessDenied') + + const treeItem = await accessDeniedNode.getTreeItem() + assert.ok(treeItem) + assert.strictEqual( + treeItem.label, + "You don't have permission to view spaces. Please contact your administrator." + ) + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.ok(treeItem.iconPath) + assert.strictEqual((treeItem.iconPath as vscode.ThemeIcon).id, 'error') + }) + }) + + describe('updateChildren', function () { + beforeEach(function () { + mockDataZoneClient.getUserId.resolves('ABCA4NU3S7PEOLDQPLXYZ:user-12345678-d061-70a4-0bf2-eeee67a6ab12') + mockDataZoneClient.getDomainId.returns('domain-123') + mockDataZoneClient.getRegion.returns('us-west-2') + mockDataZoneClient.getToolingEnvironment.resolves({ + awsAccountRegion: 'us-west-2', + provisionedResources: [{ name: 'sageMakerDomainId', value: 'sagemaker-domain-123' }], + } as any) + }) + + it('filters spaces by current user ownership', async function () { + const spaceApps = new Map([ + [ + 'space1', + { + DomainId: 'domain-123', + OwnershipSettingsSummary: { OwnerUserProfileName: 'user-12345' }, + DomainSpaceKey: 'space1', + }, + ], + [ + 'space2', + { + DomainId: 'domain-123', + OwnershipSettingsSummary: { OwnerUserProfileName: 'other-user' }, + DomainSpaceKey: 'space2', + }, + ], + ]) + const domains = new Map([['domain-123', { DomainId: 'domain-123' }]]) + + mockSagemakerClient.fetchSpaceAppsAndDomains.resolves([spaceApps, domains]) + + await spacesNode['updateChildren']() + + assert.strictEqual(spacesNode['spaceApps'].size, 1) + assert(spacesNode['spaceApps'].has('space1')) + assert(!spacesNode['spaceApps'].has('space2')) + }) + + it('creates space nodes for filtered spaces', async function () { + const spaceApps = new Map([ + [ + 'space1', + { + DomainId: 'domain-123', + OwnershipSettingsSummary: { OwnerUserProfileName: 'user-12345' }, + DomainSpaceKey: 'space1', + }, + ], + ]) + const domains = new Map([['domain-123', { DomainId: 'domain-123' }]]) + + mockSagemakerClient.fetchSpaceAppsAndDomains.resolves([spaceApps, domains]) + + await spacesNode['updateChildren']() + + assert.strictEqual(spacesNode['sagemakerSpaceNodes'].size, 1) + assert(spacesNode['sagemakerSpaceNodes'].has('space1')) + }) + + it('throws AccessDeniedException when fetchSpaceAppsAndDomains fails with access denied', async function () { + const accessDeniedError = new Error('Access denied to spaces') + accessDeniedError.name = 'AccessDeniedException' + mockSagemakerClient.fetchSpaceAppsAndDomains.rejects(accessDeniedError) + + await assert.rejects(async () => await spacesNode['updateChildren'](), /Access denied to spaces/) + }) + }) + + describe('IAM mode error handling', function () { + beforeEach(function () { + // Add getIamPrincipalArn stub to mockAuthProvider + mockAuthProvider.getIamPrincipalArn = sinon.stub().resolves('arn:aws:iam::123456789012:user/test-user') + }) + + it('should return no user profile error node when NoUserProfileFound error is thrown', async function () { + const noProfileError = new ToolkitError('No user profile found for IAM principal', { + code: SmusErrorCodes.NoUserProfileFound, + }) + const updateChildrenStub = sinon.stub(spacesNode as any, 'updateChildren') + updateChildrenStub.rejects(noProfileError) + + const children = await spacesNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].id, 'smusNoUserProfile') + + const treeItem = await children[0].getTreeItem() + assert.strictEqual(treeItem.label, 'No spaces found for IAM principal') + }) + + it('should return no user profile error node when NoGroupProfileFound error is thrown', async function () { + const noProfileError = new ToolkitError('No group profile found for IAM role', { + code: SmusErrorCodes.NoGroupProfileFound, + }) + const updateChildrenStub = sinon.stub(spacesNode as any, 'updateChildren') + updateChildrenStub.rejects(noProfileError) + + const children = await spacesNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].id, 'smusNoUserProfile') + + const treeItem = await children[0].getTreeItem() + assert.strictEqual(treeItem.label, 'No spaces found for IAM principal') + }) + + it('should return access denied error node when IAM mode returns AccessDeniedException', async function () { + const accessDeniedError = new Error("You don't have permissions to access this resource") + accessDeniedError.name = 'AccessDeniedException' + const updateChildrenStub = sinon.stub(spacesNode as any, 'updateChildren') + updateChildrenStub.rejects(accessDeniedError) + + const children = await spacesNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].id, 'smusAccessDenied') + }) + + it('should return user profile error node when IAM mode returns generic error', async function () { + const genericError = new Error('Failed to retrieve user profile information') + const updateChildrenStub = sinon.stub(spacesNode as any, 'updateChildren') + updateChildrenStub.rejects(genericError) + + const children = await spacesNode.getChildren() + + assert.strictEqual(children.length, 1) + assert.strictEqual(children[0].id, 'smusUserProfileError') + + const treeItem = await children[0].getTreeItem() + assert.strictEqual(treeItem.label, 'Failed to retrieve spaces. Please try again.') + }) + }) + + describe('getUserProfileIdForIamAuthMode - IAM user flow', function () { + let createDZClientStub: sinon.SinonStub + let getContextStub: sinon.SinonStub + + beforeEach(function () { + getContextStub = vscodeUtils.getContext as sinon.SinonStub + getContextStub.withArgs('aws.smus.isIamMode').returns(true) + createDZClientStub = sinon.stub(utils, 'createDZClientBaseOnDomainMode') + }) + + afterEach(function () { + createDZClientStub.restore() + }) + + it('should use GetUserProfile API for IAM user', async function () { + const mockUserArn = 'arn:aws:iam::123456789012:user/test-user' + const mockUserProfileId = 'up_user123' + + mockAuthProvider.getIamPrincipalArn = sinon.stub().resolves(mockUserArn) + mockAuthProvider.getDomainId = sinon.stub().returns('domain-123') + + const mockGetUserProfileId = sinon.stub().resolves(mockUserProfileId) + mockDataZoneClient.getUserProfileIdForIamPrincipal = mockGetUserProfileId as any + createDZClientStub.resolves(mockDataZoneClient) + + const result = await spacesNode['getUserProfileIdForIamAuthMode']() + + assert.strictEqual(result, mockUserProfileId) + assert(mockGetUserProfileId.calledWith(mockUserArn, 'domain-123')) + }) + + it('should throw error when IAM user profile not found', async function () { + const mockUserArn = 'arn:aws:iam::123456789012:user/test-user' + + mockAuthProvider.getIamPrincipalArn = sinon.stub().resolves(mockUserArn) + mockAuthProvider.getDomainId = sinon.stub().returns('domain-123') + + mockDataZoneClient.getUserProfileIdForIamPrincipal = sinon.stub().resolves(undefined) as any + createDZClientStub.resolves(mockDataZoneClient) + + await assert.rejects( + async () => await spacesNode['getUserProfileIdForIamAuthMode'](), + /No user profile found for IAM user/ + ) + }) + + it('should throw error when caller ARN cannot be retrieved', async function () { + mockAuthProvider.getIamPrincipalArn = sinon.stub().resolves(undefined) + + await assert.rejects( + async () => await spacesNode['getUserProfileIdForIamAuthMode'](), + /Unable to retrieve caller identity ARN/ + ) + }) + }) + + describe('getUserProfileIdForIamAuthMode - IAM role session flow', function () { + let mockDataZoneCustomClientHelper: any + let getInstanceStub: sinon.SinonStub + let getContextStub: sinon.SinonStub + + beforeEach(function () { + getContextStub = vscodeUtils.getContext as sinon.SinonStub + getContextStub.withArgs('aws.smus.isIamMode').returns(true) + + mockDataZoneCustomClientHelper = { + getUserProfileIdForSession: sinon.stub(), + } + + // Mock the DataZoneCustomClientHelper.getInstance + getInstanceStub = sinon + .stub(DataZoneCustomClientHelper, 'getInstance') + .returns(mockDataZoneCustomClientHelper) + }) + + afterEach(function () { + getInstanceStub.restore() + }) + + it('should use SearchUserProfile API for IAM role session', async function () { + const mockRoleArn = 'arn:aws:iam::123456789012:role/TestRole' + const mockAssumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/TestRole/test-session' + const mockUserProfileId = 'up_session123' + + mockAuthProvider.getIamPrincipalArn = sinon.stub().resolves(mockRoleArn) + mockAuthProvider.getCachedIamCallerIdentityArn = sinon.stub().resolves(mockAssumedRoleArn) + mockAuthProvider.getDomainId = sinon.stub().returns('domain-123') + mockDataZoneCustomClientHelper.getUserProfileIdForSession.resolves(mockUserProfileId) + + const result = await spacesNode['getUserProfileIdForIamAuthMode']() + + assert.strictEqual(result, mockUserProfileId) + assert( + mockDataZoneCustomClientHelper.getUserProfileIdForSession.calledWith('domain-123', mockAssumedRoleArn) + ) + }) + + it('should throw error when assumed role ARN cannot be retrieved', async function () { + const mockRoleArn = 'arn:aws:iam::123456789012:role/TestRole' + + mockAuthProvider.getIamPrincipalArn = sinon.stub().resolves(mockRoleArn) + mockAuthProvider.getCachedIamCallerIdentityArn = sinon.stub().resolves(undefined) + + await assert.rejects( + async () => await spacesNode['getUserProfileIdForIamAuthMode'](), + /Unable to retrieve assumed role ARN with session/ + ) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/utils.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/utils.test.ts new file mode 100644 index 00000000000..bc2785315f2 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/utils.test.ts @@ -0,0 +1,313 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as vscode from 'vscode' +import { + getLabel, + isLeafNode, + getIconForNodeType, + createTreeItem, + createColumnTreeItem, + createErrorTreeItem, + isRedLakeDatabase, + getTooltip, + getRedshiftTypeFromHost, + isRedLakeCatalog, + isS3TablesCatalog, +} from '../../../../sagemakerunifiedstudio/explorer/nodes/utils' +import { NodeType, ConnectionType, RedshiftType } from '../../../../sagemakerunifiedstudio/explorer/nodes/types' + +describe('utils', function () { + describe('getLabel', function () { + it('should return container labels for container nodes', function () { + assert.strictEqual(getLabel({ id: 'test', nodeType: NodeType.REDSHIFT_TABLE, isContainer: true }), 'Tables') + assert.strictEqual(getLabel({ id: 'test', nodeType: NodeType.REDSHIFT_VIEW, isContainer: true }), 'Views') + assert.strictEqual( + getLabel({ id: 'test', nodeType: NodeType.REDSHIFT_FUNCTION, isContainer: true }), + 'Functions' + ) + assert.strictEqual( + getLabel({ id: 'test', nodeType: NodeType.REDSHIFT_STORED_PROCEDURE, isContainer: true }), + 'Stored Procedures' + ) + }) + + it('should return path label when available', function () { + assert.strictEqual( + getLabel({ id: 'test', nodeType: NodeType.S3_FILE, path: { label: 'custom-label' } }), + 'custom-label' + ) + }) + + it('should return S3 folder name with trailing slash', function () { + assert.strictEqual( + getLabel({ id: 'test', nodeType: NodeType.S3_FOLDER, path: { key: 'folder/subfolder/' } }), + 'subfolder/' + ) + }) + + it('should return S3 file name', function () { + assert.strictEqual( + getLabel({ id: 'test', nodeType: NodeType.S3_FILE, path: { key: 'folder/file.txt' } }), + 'file.txt' + ) + }) + + it('should return last part of ID as fallback', function () { + assert.strictEqual(getLabel({ id: 'parent/child/node', nodeType: NodeType.CONNECTION }), 'node') + }) + }) + + describe('isLeafNode', function () { + it('should return false for container nodes', function () { + assert.strictEqual(isLeafNode({ nodeType: NodeType.REDSHIFT_TABLE, isContainer: true }), false) + }) + + it('should return true for leaf node types', function () { + assert.strictEqual(isLeafNode({ nodeType: NodeType.S3_FILE }), true) + assert.strictEqual(isLeafNode({ nodeType: NodeType.REDSHIFT_COLUMN }), true) + assert.strictEqual(isLeafNode({ nodeType: NodeType.ERROR }), true) + assert.strictEqual(isLeafNode({ nodeType: NodeType.LOADING }), true) + assert.strictEqual(isLeafNode({ nodeType: NodeType.EMPTY }), true) + }) + + it('should return false for non-leaf node types', function () { + assert.strictEqual(isLeafNode({ nodeType: NodeType.CONNECTION }), false) + assert.strictEqual(isLeafNode({ nodeType: NodeType.REDSHIFT_CLUSTER }), false) + }) + }) + + describe('getIconForNodeType', function () { + it('should return correct icons for different node types', function () { + const errorIcon = getIconForNodeType(NodeType.ERROR) + const loadingIcon = getIconForNodeType(NodeType.LOADING) + + assert.ok(errorIcon instanceof vscode.ThemeIcon) + assert.strictEqual((errorIcon as vscode.ThemeIcon).id, 'error') + assert.ok(loadingIcon instanceof vscode.ThemeIcon) + assert.strictEqual((loadingIcon as vscode.ThemeIcon).id, 'loading~spin') + }) + + it('should return different icons for container vs non-container nodes', function () { + const containerIcon = getIconForNodeType(NodeType.REDSHIFT_TABLE, true) + const nonContainerIcon = getIconForNodeType(NodeType.REDSHIFT_TABLE, false) + + assert.ok(containerIcon instanceof vscode.ThemeIcon) + assert.ok(nonContainerIcon instanceof vscode.ThemeIcon) + assert.strictEqual((containerIcon as vscode.ThemeIcon).id, 'table') + assert.strictEqual((nonContainerIcon as vscode.ThemeIcon).id, 'aws-redshift-table') + }) + + it('should return custom icon for GLUE_CATALOG', function () { + const catalogIcon = getIconForNodeType(NodeType.GLUE_CATALOG) + + // The catalog icon should be a custom icon, not a ThemeIcon + assert.ok(catalogIcon) + // We can't easily test the exact icon path in unit tests, but we can verify it's not a ThemeIcon + assert.ok( + !(catalogIcon instanceof vscode.ThemeIcon) || + (catalogIcon as any).id === 'aws-sagemakerunifiedstudio-catalog' + ) + }) + }) + + describe('createTreeItem', function () { + it('should create tree item with correct properties', function () { + const item = createTreeItem('Test Label', NodeType.CONNECTION, false, false, 'Test Tooltip') + + assert.strictEqual(item.label, 'Test Label') + assert.strictEqual(item.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) + assert.strictEqual(item.contextValue, NodeType.CONNECTION) + assert.strictEqual(item.tooltip, 'Test Tooltip') + }) + + it('should create leaf node with None collapsible state', function () { + const item = createTreeItem('Leaf Node', NodeType.S3_FILE, true) + + assert.strictEqual(item.collapsibleState, vscode.TreeItemCollapsibleState.None) + }) + }) + + describe('createColumnTreeItem', function () { + it('should create column tree item with type description', function () { + const item = createColumnTreeItem('column_name', 'VARCHAR(255)', NodeType.REDSHIFT_COLUMN) + + assert.strictEqual(item.label, 'column_name') + assert.strictEqual(item.description, 'VARCHAR(255)') + assert.strictEqual(item.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.strictEqual(item.contextValue, NodeType.REDSHIFT_COLUMN) + assert.strictEqual(item.tooltip, 'column_name: VARCHAR(255)') + }) + }) + + describe('createErrorTreeItem', function () { + it('should create error tree item', function () { + const item = createErrorTreeItem('Error message') + + assert.strictEqual(item.label, 'Error message') + assert.strictEqual(item.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.ok(item.iconPath instanceof vscode.ThemeIcon) + assert.strictEqual((item.iconPath as vscode.ThemeIcon).id, 'error') + }) + }) + + describe('isRedLakeDatabase', function () { + it('should return true for RedLake database names', function () { + assert.strictEqual(isRedLakeDatabase('database@catalog'), true) + assert.strictEqual(isRedLakeDatabase('my-db@my-catalog'), true) + assert.strictEqual(isRedLakeDatabase('test_db@test_catalog'), true) + }) + + it('should return false for regular database names', function () { + assert.strictEqual(isRedLakeDatabase('regular_database'), false) + assert.strictEqual(isRedLakeDatabase('dev'), false) + assert.strictEqual(isRedLakeDatabase(''), false) + assert.strictEqual(isRedLakeDatabase(undefined), false) + }) + }) + + describe('getTooltip', function () { + it('should return correct tooltip for connection nodes', function () { + const redshiftData = { + id: 'conn1', + nodeType: NodeType.CONNECTION, + connectionType: ConnectionType.REDSHIFT, + } + const s3Data = { + id: 'conn2', + nodeType: NodeType.CONNECTION, + connectionType: ConnectionType.S3, + } + + assert.strictEqual(getTooltip(redshiftData), 'Redshift Connection: conn1') + assert.strictEqual(getTooltip(s3Data), 'Connection: conn2\nType: S3') + }) + + it('should return correct tooltip for S3 nodes', function () { + const bucketData = { + id: 'bucket1', + nodeType: NodeType.S3_BUCKET, + path: { bucket: 'my-bucket' }, + } + const fileData = { + id: 'file1', + nodeType: NodeType.S3_FILE, + path: { bucket: 'my-bucket', key: 'folder/file.txt' }, + } + + assert.strictEqual(getTooltip(bucketData), 'S3 Bucket: my-bucket') + assert.strictEqual(getTooltip(fileData), 'File: file.txt\nBucket: my-bucket') + }) + + it('should return correct tooltip for Redshift container nodes', function () { + const containerData = { + id: 'tables', + nodeType: NodeType.REDSHIFT_TABLE, + isContainer: true, + path: { schema: 'public' }, + } + + assert.strictEqual(getTooltip(containerData), 'Tables in public') + }) + + it('should return correct tooltip for Redshift object nodes', function () { + const tableData = { + id: 'table1', + nodeType: NodeType.REDSHIFT_TABLE, + path: { schema: 'public' }, + } + + assert.strictEqual(getTooltip(tableData), 'Table: public.table1') + }) + }) + + describe('getRedshiftTypeFromHost', function () { + it('should return undefined for invalid hosts', function () { + assert.strictEqual(getRedshiftTypeFromHost(undefined), undefined) + assert.strictEqual(getRedshiftTypeFromHost(''), undefined) + assert.strictEqual(getRedshiftTypeFromHost('invalid-host'), undefined) + }) + + it('should identify serverless hosts', function () { + const serverlessHost = 'workgroup.123456789012.us-east-1.redshift-serverless.amazonaws.com' + assert.strictEqual(getRedshiftTypeFromHost(serverlessHost), RedshiftType.Serverless) + }) + + it('should identify cluster hosts', function () { + const clusterHost = 'cluster.123456789012.us-east-1.redshift.amazonaws.com' + assert.strictEqual(getRedshiftTypeFromHost(clusterHost), RedshiftType.Cluster) + }) + + it('should handle hosts with port numbers', function () { + const hostWithPort = 'cluster.123456789012.us-east-1.redshift.amazonaws.com:5439' + assert.strictEqual(getRedshiftTypeFromHost(hostWithPort), RedshiftType.Cluster) + }) + + it('should return undefined for unrecognized domains', function () { + const unknownHost = 'host.example.com' + assert.strictEqual(getRedshiftTypeFromHost(unknownHost), undefined) + }) + }) + + describe('isRedLakeCatalog', function () { + it('should return true for RedLake catalogs with FederatedCatalog connection', function () { + const catalog = { + FederatedCatalog: { + ConnectionName: 'aws:redshift', + }, + } + assert.strictEqual(isRedLakeCatalog(catalog), true) + }) + + it('should return true for RedLake catalogs with CatalogProperties', function () { + const catalog = { + CatalogProperties: { + DataLakeAccessProperties: { + CatalogType: 'aws:redshift', + }, + }, + } + assert.strictEqual(isRedLakeCatalog(catalog), true) + }) + + it('should return false for non-RedLake catalogs', function () { + const catalog = { + FederatedCatalog: { + ConnectionName: 'aws:s3tables', + }, + } + assert.strictEqual(isRedLakeCatalog(catalog), false) + }) + + it('should return false for undefined catalog', function () { + assert.strictEqual(isRedLakeCatalog(undefined), false) + }) + }) + + describe('isS3TablesCatalog', function () { + it('should return true for S3 Tables catalogs', function () { + const catalog = { + FederatedCatalog: { + ConnectionName: 'aws:s3tables', + }, + } + assert.strictEqual(isS3TablesCatalog(catalog), true) + }) + + it('should return false for non-S3 Tables catalogs', function () { + const catalog = { + FederatedCatalog: { + ConnectionName: 'aws:redshift', + }, + } + assert.strictEqual(isS3TablesCatalog(catalog), false) + }) + + it('should return false for undefined catalog', function () { + assert.strictEqual(isS3TablesCatalog(undefined), false) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/shared/client/clientStore.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/client/clientStore.test.ts new file mode 100644 index 00000000000..e2c14ace96a --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/client/clientStore.test.ts @@ -0,0 +1,148 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { ConnectionClientStore } from '../../../../sagemakerunifiedstudio/shared/client/connectionClientStore' +import { S3Client } from '../../../../sagemakerunifiedstudio/shared/client/s3Client' +import { SQLWorkbenchClient } from '../../../../sagemakerunifiedstudio/shared/client/sqlWorkbenchClient' +import { GlueClient } from '../../../../sagemakerunifiedstudio/shared/client/glueClient' +import { GlueCatalogClient } from '../../../../sagemakerunifiedstudio/shared/client/glueCatalogClient' +import { ConnectionCredentialsProvider } from '../../../../sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider' + +describe('ClientStore', function () { + let sandbox: sinon.SinonSandbox + let clientStore: ConnectionClientStore + + const mockCredentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + + beforeEach(function () { + sandbox = sinon.createSandbox() + clientStore = ConnectionClientStore.getInstance() + }) + + afterEach(function () { + sandbox.restore() + clientStore.clearAll() + }) + + describe('getInstance', function () { + it('should return singleton instance', function () { + const instance1 = ConnectionClientStore.getInstance() + const instance2 = ConnectionClientStore.getInstance() + assert.strictEqual(instance1, instance2) + }) + }) + + describe('getClient', function () { + it('should create and cache client', function () { + const factory = sandbox.stub().returns({ test: 'client' }) + + const client1 = clientStore.getClient('conn-1', 'TestClient', factory) + const client2 = clientStore.getClient('conn-1', 'TestClient', factory) + + assert.strictEqual(client1, client2) + assert.ok(factory.calledOnce) + }) + + it('should create separate clients for different connections', function () { + const factory = sandbox.stub() + factory.onFirstCall().returns({ test: 'client1' }) + factory.onSecondCall().returns({ test: 'client2' }) + + const client1 = clientStore.getClient('conn-1', 'TestClient', factory) + const client2 = clientStore.getClient('conn-2', 'TestClient', factory) + + assert.notStrictEqual(client1, client2) + assert.ok(factory.calledTwice) + }) + }) + + describe('getS3Client', function () { + it('should create S3Client with credentials provider', function () { + sandbox.stub(S3Client.prototype, 'constructor' as any) + + const client = clientStore.getS3Client( + 'conn-1', + 'us-east-1', + mockCredentialsProvider as ConnectionCredentialsProvider + ) + + assert.ok(client instanceof S3Client) + }) + }) + + describe('getSQLWorkbenchClient', function () { + it('should create SQLWorkbenchClient with credentials provider', function () { + const stub = sandbox.stub(SQLWorkbenchClient, 'createWithCredentials').returns({} as any) + + clientStore.getSQLWorkbenchClient( + 'conn-1', + 'us-east-1', + mockCredentialsProvider as ConnectionCredentialsProvider + ) + + assert.ok(stub.calledOnce) + }) + }) + + describe('getGlueClient', function () { + it('should create GlueClient with credentials provider', function () { + sandbox.stub(GlueClient.prototype, 'constructor' as any) + + const client = clientStore.getGlueClient( + 'conn-1', + 'us-east-1', + mockCredentialsProvider as ConnectionCredentialsProvider + ) + + assert.ok(client instanceof GlueClient) + }) + }) + + describe('getGlueCatalogClient', function () { + it('should create GlueCatalogClient with credentials provider', function () { + const stub = sandbox.stub(GlueCatalogClient, 'createWithCredentials').returns({} as any) + + clientStore.getGlueCatalogClient( + 'conn-1', + 'us-east-1', + mockCredentialsProvider as ConnectionCredentialsProvider + ) + + assert.ok(stub.calledOnce) + }) + }) + + describe('clearConnection', function () { + it('should clear cached clients for specific connection', function () { + const factory = sandbox.stub().returns({ test: 'client' }) + + clientStore.getClient('conn-1', 'TestClient', factory) + clientStore.clearConnection('conn-1') + clientStore.getClient('conn-1', 'TestClient', factory) + + assert.strictEqual(factory.callCount, 2) + }) + }) + + describe('clearAll', function () { + it('should clear all cached clients', function () { + const factory = sandbox.stub().returns({ test: 'client' }) + + clientStore.getClient('conn-1', 'TestClient', factory) + clientStore.clearAll() + clientStore.getClient('conn-1', 'TestClient', factory) + + assert.strictEqual(factory.callCount, 2) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/shared/client/credentialsAdapter.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/client/credentialsAdapter.test.ts new file mode 100644 index 00000000000..cfb2cfbce6e --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/client/credentialsAdapter.test.ts @@ -0,0 +1,53 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import * as AWS from 'aws-sdk' +import { adaptConnectionCredentialsProvider } from '../../../../sagemakerunifiedstudio/shared/client/credentialsAdapter' + +describe('credentialsAdapter', function () { + let sandbox: sinon.SinonSandbox + let mockConnectionCredentialsProvider: any + + beforeEach(function () { + sandbox = sinon.createSandbox() + mockConnectionCredentialsProvider = { + getCredentials: sandbox.stub(), + } + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('adaptConnectionCredentialsProvider', function () { + it('should create CredentialProviderChain', function () { + const chain = adaptConnectionCredentialsProvider(mockConnectionCredentialsProvider) + assert.ok(chain instanceof AWS.CredentialProviderChain) + }) + + it('should create credentials with provider function', function () { + const chain = adaptConnectionCredentialsProvider(mockConnectionCredentialsProvider) + assert.ok(chain.providers) + assert.strictEqual(chain.providers.length, 1) + assert.strictEqual(typeof chain.providers[0], 'function') + }) + + it('should create AWS Credentials object', function () { + const chain = adaptConnectionCredentialsProvider(mockConnectionCredentialsProvider) + const provider = chain.providers[0] as () => AWS.Credentials + const credentials = provider() + assert.ok(credentials instanceof AWS.Credentials) + }) + + it('should set needsRefresh to always return true', function () { + const chain = adaptConnectionCredentialsProvider(mockConnectionCredentialsProvider) + const provider = chain.providers[0] as () => AWS.Credentials + const credentials = provider() + assert.strictEqual(credentials.needsRefresh(), true) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/shared/client/datazoneClient.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/client/datazoneClient.test.ts new file mode 100644 index 00000000000..91a6a092bde --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/client/datazoneClient.test.ts @@ -0,0 +1,636 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { DataZoneClient } from '../../../../sagemakerunifiedstudio/shared/client/datazoneClient' +import { SmusAuthenticationProvider } from '../../../../sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider' +import { GetEnvironmentCommandOutput } from '@aws-sdk/client-datazone/dist-types/commands/GetEnvironmentCommand' +import { DefaultStsClient } from '../../../../shared/clients/stsClient' +import { SmusUtils, SmusErrorCodes } from '../../../../sagemakerunifiedstudio/shared/smusUtils' +import { ToolkitError } from '../../../../shared/errors' + +describe('DataZoneClient', () => { + let dataZoneClient: DataZoneClient + let mockAuthProvider: any + const testDomainId = 'dzd_domainId' + const testRegion = 'us-east-2' + + beforeEach(async () => { + // Create mock connection object + const mockConnection = { + id: 'connection-id', + domainId: testDomainId, + ssoRegion: testRegion, + } + + // Create mock auth provider + mockAuthProvider = { + isConnected: sinon.stub().returns(true), + getDomainId: sinon.stub().returns(testDomainId), + getDomainRegion: sinon.stub().returns(testRegion), + activeConnection: mockConnection, + onDidChangeActiveConnection: sinon.stub().returns({ + dispose: sinon.stub(), + }), + secondaryAuth: { + state: { + get: sinon.stub().returns({ + 'connection-id': { + profileName: 'test-profile', + }, + }), + }, + }, + getCredentialsProviderForIamProfile: sinon.stub(), + } as any + + // Create mock credentials provider + const mockCredentialsProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + getCredentialsId: () => ({ credentialSource: 'temp' as const, credentialTypeId: 'test' }), + getProviderType: () => 'temp' as const, + getTelemetryType: () => 'other' as any, + getDefaultRegion: () => testRegion, + getHashCode: () => 'test-hash', + canAutoConnect: () => Promise.resolve(false), + isAvailable: () => Promise.resolve(true), + } + + // Set up the DataZoneClient using createWithCredentials + dataZoneClient = DataZoneClient.createWithCredentials(testRegion, testDomainId, mockCredentialsProvider) + }) + + afterEach(() => { + sinon.restore() + }) + + describe('createWithCredentials', () => { + it('should create new instance with credentials', () => { + const mockCredentialsProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + getCredentialsId: () => ({ credentialSource: 'temp' as const, credentialTypeId: 'test' }), + getProviderType: () => 'temp' as const, + getTelemetryType: () => 'other' as any, + getDefaultRegion: () => testRegion, + getHashCode: () => 'test-hash', + canAutoConnect: () => Promise.resolve(false), + isAvailable: () => Promise.resolve(true), + } + + const instance = DataZoneClient.createWithCredentials(testRegion, testDomainId, mockCredentialsProvider) + assert.ok(instance) + assert.strictEqual(instance.getRegion(), testRegion) + assert.strictEqual(instance.getDomainId(), testDomainId) + }) + }) + + describe('getRegion', () => { + it('should return configured region', () => { + const result = dataZoneClient.getRegion() + assert.strictEqual(typeof result, 'string') + assert.ok(result.length > 0) + }) + }) + + describe('listProjects', () => { + it('should list projects with pagination', async () => { + const mockDataZone = { + listProjects: sinon.stub().resolves({ + items: [ + { + id: 'project-1', + name: 'Project 1', + description: 'First project', + createdAt: new Date('2023-01-01'), + updatedAt: new Date('2023-01-02'), + }, + ], + nextToken: 'next-token', + }), + } + + // Mock the getDataZoneClient method + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + const result = await dataZoneClient.listProjects({ + maxResults: 10, + }) + + assert.strictEqual(result.projects.length, 1) + assert.strictEqual(result.projects[0].id, 'project-1') + assert.strictEqual(result.projects[0].name, 'Project 1') + assert.strictEqual(result.projects[0].domainId, testDomainId) + assert.strictEqual(result.nextToken, 'next-token') + }) + + it('should handle empty results', async () => { + const mockDataZone = { + listProjects: sinon.stub().resolves({ + items: [], + nextToken: undefined, + }), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + const result = await dataZoneClient.listProjects() + + assert.strictEqual(result.projects.length, 0) + assert.strictEqual(result.nextToken, undefined) + }) + + it('should handle API errors', async () => { + const error = new Error('API Error') + sinon.stub(dataZoneClient as any, 'getDataZoneClient').rejects(error) + + await assert.rejects(() => dataZoneClient.listProjects(), error) + }) + }) + + describe('getProjectDefaultEnvironmentCreds', () => { + it('should get environment credentials for project', async () => { + const mockCredentials = { + accessKeyId: 'AKIATEST', + secretAccessKey: 'secret', + sessionToken: 'token', + } + + const mockDataZone = { + listEnvironmentBlueprints: sinon.stub().resolves({ + items: [{ id: 'blueprint-1', name: 'Tooling' }], + }), + listEnvironments: sinon.stub().resolves({ + items: [{ id: 'env-1', name: 'Tooling' }], + }), + getEnvironmentCredentials: sinon.stub().resolves(mockCredentials), + } + + // Mock getToolingBlueprintName to return 'Tooling' + sinon.stub(dataZoneClient as any, 'getToolingBlueprintName').returns('Tooling') + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + const result = await dataZoneClient.getProjectDefaultEnvironmentCreds('project-1') + + assert.deepStrictEqual(result, mockCredentials) + assert.ok( + mockDataZone.listEnvironmentBlueprints.calledWith({ + domainIdentifier: testDomainId, + managed: true, + name: 'Tooling', + }) + ) + assert.ok( + mockDataZone.listEnvironments.calledWith({ + domainIdentifier: testDomainId, + projectIdentifier: 'project-1', + environmentBlueprintIdentifier: 'blueprint-1', + provider: 'Amazon SageMaker', + }) + ) + assert.ok( + mockDataZone.getEnvironmentCredentials.calledWith({ + domainIdentifier: testDomainId, + environmentIdentifier: 'env-1', + }) + ) + }) + + it('should throw error when tooling blueprint not found', async () => { + const mockDataZone = { + listEnvironmentBlueprints: sinon.stub().resolves({ + items: [], + }), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + await assert.rejects( + () => dataZoneClient.getProjectDefaultEnvironmentCreds('project-1'), + /Failed to get tooling blueprint/ + ) + }) + + it('should throw error when default environment not found', async () => { + const mockDataZone = { + listEnvironmentBlueprints: sinon.stub().resolves({ + items: [{ id: 'blueprint-1', name: 'Tooling' }], + }), + listEnvironments: sinon.stub().resolves({ + items: [], + }), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + await assert.rejects( + () => dataZoneClient.getProjectDefaultEnvironmentCreds('project-1'), + /Failed to find default Tooling environment/ + ) + }) + }) + + describe('fetchAllProjects', function () { + it('fetches all projects by handling pagination', async function () { + // Create a stub for listProjects that returns paginated resultssults + const listProjectsStub = sinon.stub() + + // First call returns first page with nextToken + listProjectsStub.onFirstCall().resolves({ + projects: [ + { + id: 'project-1', + name: 'Project 1', + description: 'First project', + domainId: testDomainId, + }, + ], + nextToken: 'next-page-token', + }) + + // Second call returns second page with no nextToken + listProjectsStub.onSecondCall().resolves({ + projects: [ + { + id: 'project-2', + name: 'Project 2', + description: 'Second project', + domainId: testDomainId, + }, + ], + nextToken: undefined, + }) + + // Replace the listProjects method with our stub + dataZoneClient.listProjects = listProjectsStub + + // Call fetchAllProjects + const result = await dataZoneClient.fetchAllProjects() + + // Verify results + assert.strictEqual(result.length, 2) + assert.strictEqual(result[0].id, 'project-1') + assert.strictEqual(result[1].id, 'project-2') + + // Verify listProjects was called correctly + assert.strictEqual(listProjectsStub.callCount, 2) + assert.deepStrictEqual(listProjectsStub.firstCall.args[0], { + maxResults: 50, + nextToken: undefined, + }) + assert.deepStrictEqual(listProjectsStub.secondCall.args[0], { + maxResults: 50, + nextToken: 'next-page-token', + }) + }) + + it('returns empty array when no projects found', async function () { + // Create a stub for listProjects that returns empty results + const listProjectsStub = sinon.stub().resolves({ + projects: [], + nextToken: undefined, + }) + + // Replace the listProjects method with our stub + dataZoneClient.listProjects = listProjectsStub + + // Call fetchAllProjects + const result = await dataZoneClient.fetchAllProjects() + + // Verify results + assert.strictEqual(result.length, 0) + assert.strictEqual(listProjectsStub.callCount, 1) + }) + + it('handles errors gracefully', async function () { + // Create a stub for listProjects that throws an error + const listProjectsStub = sinon.stub().rejects(new Error('API error')) + + // Replace the listProjects method with our stub + dataZoneClient.listProjects = listProjectsStub + + // Call fetchAllProjects and expect it to throw + await assert.rejects(() => dataZoneClient.fetchAllProjects(), /API error/) + }) + }) + + describe('getToolingEnvironmentId', () => { + it('should get tooling environment ID successfully', async () => { + const mockDataZone = { + listEnvironmentBlueprints: sinon.stub().resolves({ + items: [{ id: 'blueprint-1', name: 'Tooling' }], + }), + listEnvironments: sinon.stub().resolves({ + items: [{ id: 'env-1', name: 'Tooling' }], + }), + } + + // Mock getToolingBlueprintName to return 'Tooling' + sinon.stub(dataZoneClient as any, 'getToolingBlueprintName').returns('Tooling') + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + const result = await dataZoneClient.getToolingEnvironmentId('domain-1', 'project-1') + + assert.strictEqual(result, 'env-1') + }) + + it('should handle listEnvironmentBlueprints error', async () => { + const error = new Error('Blueprint API Error') + const mockDataZone = { + listEnvironmentBlueprints: sinon.stub().rejects(error), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + await assert.rejects(() => dataZoneClient.getToolingEnvironmentId('domain-1', 'project-1'), error) + }) + + it('should handle listEnvironments error', async () => { + const error = new Error('Environment API Error') + const mockDataZone = { + listEnvironmentBlueprints: sinon.stub().resolves({ + items: [{ id: 'blueprint-1', name: 'Tooling' }], + }), + listEnvironments: sinon.stub().rejects(error), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + await assert.rejects(() => dataZoneClient.getToolingEnvironmentId('domain-1', 'project-1'), error) + }) + }) + + describe('getToolingEnvironment', () => { + beforeEach(() => { + mockAuthProvider = {} as SmusAuthenticationProvider + }) + + it('should return environment details when successful', async () => { + const mockEnvironment: GetEnvironmentCommandOutput = { + id: 'env-123', + awsAccountRegion: 'us-east-1', + projectId: undefined, + domainId: undefined, + createdBy: undefined, + name: undefined, + provider: undefined, + $metadata: {}, + } + + const mockDataZone = { + listEnvironmentBlueprints: sinon.stub().resolves({ + items: [{ id: 'blueprint-1', name: 'Tooling' }], + }), + listEnvironments: sinon.stub().resolves({ + items: [{ id: 'env-1', name: 'Tooling' }], + }), + getEnvironment: sinon.stub().resolves(mockEnvironment), + } + + // Mock getToolingBlueprintName to return 'Tooling' + sinon.stub(dataZoneClient as any, 'getToolingBlueprintName').returns('Tooling') + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + const result = await dataZoneClient.getToolingEnvironment('project-123') + + assert.strictEqual(result, mockEnvironment) + }) + + it('should throw error when no tooling environment ID found', async () => { + const mockDataZone = { + listEnvironmentBlueprints: sinon.stub().resolves({ + items: [{ id: 'blueprint-1', name: 'Tooling' }], + }), + listEnvironments: sinon.stub().resolves({ + items: [], + }), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + await assert.rejects( + () => dataZoneClient.getToolingEnvironment('project-123'), + /No default Tooling environment found for project/ + ) + }) + + it('should throw error when getToolingEnvironmentId fails', async () => { + const mockDataZone = { + listEnvironmentBlueprints: sinon.stub().rejects(new Error('API error')), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + await assert.rejects(() => dataZoneClient.getToolingEnvironment('project-123'), /API error/) + }) + }) + + describe('fetchAllProjectMemberships', () => { + it('should fetch all project memberships with pagination', async () => { + const mockDataZone = { + listProjectMemberships: sinon.stub(), + } + + // First call returns first page with nextToken + mockDataZone.listProjectMemberships.onFirstCall().resolves({ + members: [ + { + memberDetails: { + user: { + userId: 'user-1', + }, + }, + }, + ], + nextToken: 'next-token', + }) + + // Second call returns second page without nextToken + mockDataZone.listProjectMemberships.onSecondCall().resolves({ + members: [ + { + memberDetails: { + user: { + userId: 'user-2', + }, + }, + }, + ], + nextToken: undefined, + }) + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + const result = await dataZoneClient.fetchAllProjectMemberships('project-1') + + assert.strictEqual(result.length, 2) + assert.strictEqual(result[0].memberDetails?.user?.userId, 'user-1') + assert.strictEqual(result[1].memberDetails?.user?.userId, 'user-2') + assert.strictEqual(mockDataZone.listProjectMemberships.callCount, 2) + }) + + it('should handle empty memberships', async () => { + const mockDataZone = { + listProjectMemberships: sinon.stub().resolves({ + members: [], + nextToken: undefined, + }), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + const result = await dataZoneClient.fetchAllProjectMemberships('project-1') + + assert.strictEqual(result.length, 0) + }) + + it('should handle API errors', async () => { + const error = new Error('Membership API Error') + const mockDataZone = { + listProjectMemberships: sinon.stub().rejects(error), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + await assert.rejects(() => dataZoneClient.fetchAllProjectMemberships('project-1'), error) + }) + }) + + describe('getUserProfileId', () => { + let stsClientStub: sinon.SinonStub + let convertAssumedRoleArnStub: sinon.SinonStub + let mockCredentialsProvider: any + + beforeEach(() => { + // Mock connection with ID + mockAuthProvider.activeConnection = { id: 'connection-id' } + + // Mock credentials provider + mockCredentialsProvider = { + getCredentials: sinon.stub().resolves({ + accessKeyId: 'id', + secretAccessKey: 'secret', + sessionToken: 'token', + }), + } + + mockAuthProvider.getCredentialsProviderForIamProfile.resolves(mockCredentialsProvider) + + // Stub STS client + stsClientStub = sinon.stub(DefaultStsClient.prototype, 'getCallerIdentity') + + // Stub SmusUtils method + convertAssumedRoleArnStub = sinon.stub(SmusUtils as any, 'convertAssumedRoleArnToIamRoleArn') + }) + + afterEach(() => { + stsClientStub.restore() + convertAssumedRoleArnStub.restore() + }) + + it('should successfully get user profile ID with role ARN', async () => { + const mockRoleArn = 'arn:aws:iam::123456789012:role/service-role/MyRole' + const mockUserProfileId = 'user-profile-123' + + const mockDataZone = { + getUserProfile: sinon.stub().resolves({ + id: mockUserProfileId, + userIdentifier: mockRoleArn, + }), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + const result = await dataZoneClient.getUserProfileIdForIamPrincipal(mockRoleArn) + + assert.strictEqual(result, mockUserProfileId) + assert.ok( + mockDataZone.getUserProfile.calledWith({ + domainIdentifier: testDomainId, + userIdentifier: mockRoleArn, + }) + ) + }) + + it('should handle DataZone getUserProfile API failure', async () => { + const mockRoleArn = 'arn:aws:iam::123456789012:role/service-role/MyRole' + const datazoneError = new Error('DataZone API Error') + + const mockDataZone = { + getUserProfile: sinon.stub().rejects(datazoneError), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + await assert.rejects( + async () => { + await dataZoneClient.getUserProfileIdForIamPrincipal(mockRoleArn) + }, + (error: Error) => { + assert.ok(error instanceof ToolkitError) + assert.ok(error.message.includes('Failed to get user profile ID')) + return true + } + ) + }) + + it('should get user profile ID for IAM user ARN', async () => { + const mockUserArn = 'arn:aws:iam::123456789012:user/test-user' + const mockUserProfileId = 'user-profile-456' + + const mockDataZone = { + getUserProfile: sinon.stub().resolves({ + id: mockUserProfileId, + userIdentifier: mockUserArn, + }), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + const result = await dataZoneClient.getUserProfileIdForIamPrincipal(mockUserArn) + + assert.strictEqual(result, mockUserProfileId) + assert.ok( + mockDataZone.getUserProfile.calledWith({ + domainIdentifier: testDomainId, + userIdentifier: mockUserArn, + }) + ) + }) + + it('should throw error when user profile ID is not returned', async () => { + const mockUserArn = 'arn:aws:iam::123456789012:user/test-user' + + const mockDataZone = { + getUserProfile: sinon.stub().resolves({ + // No id field + }), + } + + sinon.stub(dataZoneClient as any, 'getDataZoneClient').resolves(mockDataZone) + + await assert.rejects( + async () => { + await dataZoneClient.getUserProfileIdForIamPrincipal(mockUserArn) + }, + (error: Error) => { + assert.ok(error instanceof ToolkitError) + assert.strictEqual((error as ToolkitError).code, SmusErrorCodes.NoUserProfileFound) + return true + } + ) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/shared/client/datazoneCustomClientHelper.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/client/datazoneCustomClientHelper.test.ts new file mode 100644 index 00000000000..59dfd0c7d97 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/client/datazoneCustomClientHelper.test.ts @@ -0,0 +1,1221 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { DataZoneCustomClientHelper } from '../../../../sagemakerunifiedstudio/shared/client/datazoneCustomClientHelper' +import * as DataZoneCustomClient from '../../../../sagemakerunifiedstudio/shared/client/datazonecustomclient' + +type DataZoneDomain = DataZoneCustomClient.Types.DomainSummary + +describe('DataZoneCustomClientHelper', () => { + let client: DataZoneCustomClientHelper + let mockAuthProvider: any + const testRegion = 'us-east-1' + + beforeEach(() => { + // Create mock auth provider + mockAuthProvider = { + isConnected: sinon.stub().returns(true), + onDidChangeActiveConnection: sinon.stub().returns({ + dispose: sinon.stub(), + }), + } as any + + // Clear instances and create new client + DataZoneCustomClientHelper.dispose() + client = DataZoneCustomClientHelper.getInstance(mockAuthProvider, testRegion) + }) + + afterEach(() => { + sinon.restore() + DataZoneCustomClientHelper.dispose() + }) + + describe('getInstance', () => { + it('should return singleton instance for same region', () => { + const instance1 = DataZoneCustomClientHelper.getInstance(mockAuthProvider, testRegion) + const instance2 = DataZoneCustomClientHelper.getInstance(mockAuthProvider, testRegion) + + assert.strictEqual(instance1, instance2) + }) + + it('should create different instances for different regions', () => { + const instance1 = DataZoneCustomClientHelper.getInstance(mockAuthProvider, 'us-east-1') + const instance2 = DataZoneCustomClientHelper.getInstance(mockAuthProvider, 'us-west-2') + + assert.notStrictEqual(instance1, instance2) + }) + + it('should create new instance after dispose', () => { + const instance1 = DataZoneCustomClientHelper.getInstance(mockAuthProvider, testRegion) + DataZoneCustomClientHelper.dispose() + const instance2 = DataZoneCustomClientHelper.getInstance(mockAuthProvider, testRegion) + + assert.notStrictEqual(instance1, instance2) + }) + }) + + describe('dispose', () => { + it('should clear all instances', () => { + const instance1 = DataZoneCustomClientHelper.getInstance(mockAuthProvider, 'us-east-1') + const instance2 = DataZoneCustomClientHelper.getInstance(mockAuthProvider, 'us-west-2') + + DataZoneCustomClientHelper.dispose() + + // Should create new instance after dispose + const newInstance1 = DataZoneCustomClientHelper.getInstance(mockAuthProvider, 'us-east-1') + const newInstance2 = DataZoneCustomClientHelper.getInstance(mockAuthProvider, 'us-west-2') + + assert.notStrictEqual(instance1, newInstance1) + assert.notStrictEqual(instance2, newInstance2) + }) + }) + + describe('getRegion', () => { + it('should return configured region', () => { + const result = client.getRegion() + assert.strictEqual(result, testRegion) + }) + }) + + describe('listDomains', () => { + it('should list domains with pagination', async () => { + const mockResponse = { + items: [ + { + id: 'dzd_domain1', + name: 'Test Domain 1', + description: 'First test domain', + arn: 'arn:aws:datazone:us-east-1:123456789012:domain/dzd_domain1', + managedAccountId: '123456789012', + status: 'AVAILABLE', + portalUrl: 'https://domain1.datazone.aws', + createdAt: new Date('2023-01-01T00:00:00Z'), + lastUpdatedAt: new Date('2023-01-02T00:00:00Z'), + domainVersion: '1.0', + preferences: { DOMAIN_MODE: 'STANDARD' }, + }, + ], + nextToken: 'next-token', + } + + const mockDataZoneClient = { + listDomains: sinon.stub().returns({ + promise: () => Promise.resolve(mockResponse), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + const result = await client.listDomains({ + maxResults: 10, + status: 'AVAILABLE', + }) + + assert.strictEqual(result.domains.length, 1) + assert.strictEqual(result.domains[0].id, 'dzd_domain1') + assert.strictEqual(result.domains[0].name, 'Test Domain 1') + assert.strictEqual(result.domains[0].arn, 'arn:aws:datazone:us-east-1:123456789012:domain/dzd_domain1') + assert.strictEqual(result.domains[0].managedAccountId, '123456789012') + assert.strictEqual(result.domains[0].status, 'AVAILABLE') + assert.strictEqual(result.nextToken, 'next-token') + assert.ok(result.domains[0].createdAt instanceof Date) + assert.ok(result.domains[0].lastUpdatedAt instanceof Date) + }) + + it('should handle empty results', async () => { + const mockResponse = { + items: [], + nextToken: undefined, + } + + const mockDataZoneClient = { + listDomains: sinon.stub().returns({ + promise: () => Promise.resolve(mockResponse), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + const result = await client.listDomains() + + assert.strictEqual(result.domains.length, 0) + assert.strictEqual(result.nextToken, undefined) + }) + + it('should handle API errors', async () => { + const error = new Error('API Error') + sinon.stub(client as any, 'getDataZoneCustomClient').rejects(error) + + await assert.rejects(() => client.listDomains(), error) + }) + }) + + describe('fetchAllDomains', () => { + it('should fetch all domains by handling pagination', async () => { + const listDomainsStub = sinon.stub() + + // First call returns first page with nextToken + listDomainsStub.onFirstCall().resolves({ + domains: [ + { + id: 'dzd_domain1', + name: 'Domain 1', + arn: 'arn:aws:datazone:us-east-1:123456789012:domain/dzd_domain1', + managedAccountId: '123456789012', + status: 'AVAILABLE', + createdAt: new Date(), + } as DataZoneDomain, + ], + nextToken: 'next-page-token', + }) + + // Second call returns second page with no nextToken + listDomainsStub.onSecondCall().resolves({ + domains: [ + { + id: 'dzd_domain2', + name: 'Domain 2', + arn: 'arn:aws:datazone:us-east-1:123456789012:domain/dzd_domain2', + managedAccountId: '123456789012', + status: 'AVAILABLE', + createdAt: new Date(), + } as DataZoneDomain, + ], + nextToken: undefined, + }) + + // Replace the listDomains method with our stub + client.listDomains = listDomainsStub + + const result = await client.fetchAllDomains({ status: 'AVAILABLE' }) + + assert.strictEqual(result.length, 2) + assert.strictEqual(result[0].id, 'dzd_domain1') + assert.strictEqual(result[1].id, 'dzd_domain2') + + // Verify listDomains was called correctly + assert.strictEqual(listDomainsStub.callCount, 2) + assert.deepStrictEqual(listDomainsStub.firstCall.args[0], { + status: 'AVAILABLE', + maxResults: 25, + nextToken: undefined, + }) + assert.deepStrictEqual(listDomainsStub.secondCall.args[0], { + status: 'AVAILABLE', + maxResults: 25, + nextToken: 'next-page-token', + }) + }) + + it('should return empty array when no domains found', async () => { + const listDomainsStub = sinon.stub().resolves({ + domains: [], + nextToken: undefined, + }) + + client.listDomains = listDomainsStub + + const result = await client.fetchAllDomains() + + assert.strictEqual(result.length, 0) + assert.strictEqual(listDomainsStub.callCount, 1) + }) + + it('should handle errors gracefully', async () => { + const listDomainsStub = sinon.stub().rejects(new Error('API error')) + + client.listDomains = listDomainsStub + + await assert.rejects(() => client.fetchAllDomains(), /API error/) + }) + }) + + describe('getDomain', () => { + it('should find EXPRESS domain', async () => { + const listDomainsStub = sinon.stub() + + listDomainsStub.onFirstCall().resolves({ + domains: [ + { + id: 'dzd_standard', + name: 'Standard Domain', + arn: 'arn:aws:datazone:us-east-1:123456789012:domain/dzd_standard', + managedAccountId: '123456789012', + status: 'AVAILABLE', + createdAt: new Date(), + domainVersion: 'V2', + iamSignIns: ['IAM_ROLE'], + preferences: { DOMAIN_MODE: 'STANDARD' }, + }, + { + id: 'dzd_express', + name: 'Express Domain', + arn: 'arn:aws:datazone:us-east-1:123456789012:domain/dzd_express', + managedAccountId: '123456789012', + status: 'AVAILABLE', + createdAt: new Date(), + domainVersion: 'V2', + iamSignIns: ['IAM_ROLE', 'IAM_USER'], + preferences: { DOMAIN_MODE: 'EXPRESS' }, + }, + ] as DataZoneDomain[], + nextToken: 'next-token', + }) + + client.listDomains = listDomainsStub + + const result = await client.getIamDomain() + + assert.ok(result) + assert.strictEqual(result.id, 'dzd_express') + assert.strictEqual(result.name, 'Express Domain') + assert.strictEqual(result.preferences?.DOMAIN_MODE, 'EXPRESS') + + // Should only call once since EXPRESS domain found on first page + assert.strictEqual(listDomainsStub.callCount, 1) + }) + + it('should return undefined when no EXPRESS domain found', async () => { + const listDomainsStub = sinon.stub() + + listDomainsStub.onFirstCall().resolves({ + domains: [ + { + id: 'dzd_standard', + name: 'Standard Domain', + arn: 'arn:aws:datazone:us-east-1:123456789012:domain/dzd_standard', + managedAccountId: '123456789012', + status: 'AVAILABLE', + createdAt: new Date(), + preferences: { DOMAIN_MODE: 'STANDARD' }, + }, + ] as DataZoneDomain[], + nextToken: undefined, + }) + + client.listDomains = listDomainsStub + + const result = await client.getIamDomain() + + assert.strictEqual(result, undefined) + assert.strictEqual(listDomainsStub.callCount, 1) + }) + + it('should return undefined when no domains found', async () => { + const listDomainsStub = sinon.stub().resolves({ + domains: [], + nextToken: undefined, + }) + + client.listDomains = listDomainsStub + + const result = await client.getIamDomain() + + assert.strictEqual(result, undefined) + assert.strictEqual(listDomainsStub.callCount, 1) + }) + + it('should handle domains without preferences', async () => { + const listDomainsStub = sinon.stub() + + listDomainsStub.onFirstCall().resolves({ + domains: [ + { + id: 'dzd_no_prefs', + name: 'Domain Without Preferences', + arn: 'arn:aws:datazone:us-east-1:123456789012:domain/dzd_no_prefs', + managedAccountId: '123456789012', + status: 'AVAILABLE', + createdAt: new Date(), + // No preferences field + }, + ] as DataZoneDomain[], + nextToken: undefined, + }) + + client.listDomains = listDomainsStub + + const result = await client.getIamDomain() + + assert.strictEqual(result, undefined) + }) + + it('should handle API errors', async () => { + const listDomainsStub = sinon.stub().rejects(new Error('API error')) + + client.listDomains = listDomainsStub + + await assert.rejects(() => client.getIamDomain(), /Failed to get domain info: API error/) + }) + }) + + describe('getDomain', () => { + it('should get domain by ID successfully', async () => { + const mockDomainId = 'dzd_test123' + const mockResponse = { + id: mockDomainId, + name: 'Test Domain', + description: 'A test domain', + arn: `arn:aws:datazone:us-east-1:123456789012:domain/${mockDomainId}`, + status: 'AVAILABLE', + portalUrl: 'https://test.datazone.aws', + createdAt: '2023-01-01T00:00:00Z', + lastUpdatedAt: '2023-01-02T00:00:00Z', + domainVersion: '1.0', + preferences: { DOMAIN_MODE: 'EXPRESS' }, + } + const mockDataZoneClient = { + getDomain: sinon.stub().returns({ + promise: () => Promise.resolve(mockResponse), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + const result = await client.getDomain(mockDomainId) + + assert.strictEqual(result.id, mockDomainId) + assert.strictEqual(result.name, 'Test Domain') + assert.strictEqual(result.description, 'A test domain') + assert.strictEqual(result.arn, `arn:aws:datazone:us-east-1:123456789012:domain/${mockDomainId}`) + assert.strictEqual(result.status, 'AVAILABLE') + assert.strictEqual(result.portalUrl, 'https://test.datazone.aws') + assert.strictEqual(result.domainVersion, '1.0') + assert.deepStrictEqual(result.preferences, { DOMAIN_MODE: 'EXPRESS' }) + + // Verify the API was called with correct parameters + assert.ok(mockDataZoneClient.getDomain.calledOnce) + assert.deepStrictEqual(mockDataZoneClient.getDomain.firstCall.args[0], { + identifier: mockDomainId, + }) + }) + + it('should handle API errors when getting domain', async () => { + const mockDomainId = 'dzd_test123' + const error = new Error('Domain not found') + + const mockDataZoneClient = { + getDomain: sinon.stub().returns({ + promise: () => Promise.reject(error), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + await assert.rejects(() => client.getDomain(mockDomainId), error) + + // Verify the API was called with correct parameters + assert.ok(mockDataZoneClient.getDomain.calledOnce) + assert.deepStrictEqual(mockDataZoneClient.getDomain.firstCall.args[0], { + identifier: mockDomainId, + }) + }) + }) + + describe('searchGroupProfiles', () => { + const mockDomainId = 'dzd_test123' + + it('should search group profiles successfully', async () => { + const mockResponse = { + items: [ + { + domainId: mockDomainId, + id: 'gp_profile1', + status: 'ACTIVATED', + groupName: 'AdminGroup', + rolePrincipalArn: 'arn:aws:iam::123456789012:role/AdminRole', + rolePrincipalId: 'AIDAI123456789EXAMPLE', + }, + { + domainId: mockDomainId, + id: 'gp_profile2', + status: 'ACTIVATED', + groupName: 'DeveloperGroup', + rolePrincipalArn: 'arn:aws:iam::123456789012:role/DeveloperRole', + rolePrincipalId: 'AIDAI987654321EXAMPLE', + }, + ], + nextToken: 'next-token', + } + + const mockDataZoneClient = { + searchGroupProfiles: sinon.stub().returns({ + promise: () => Promise.resolve(mockResponse), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + const result = await client.searchGroupProfiles(mockDomainId, { + groupType: 'IAM_ROLE_SESSION_GROUP', + maxResults: 50, + }) + + assert.strictEqual(result.items.length, 2) + assert.strictEqual(result.items[0].id, 'gp_profile1') + assert.strictEqual(result.items[0].rolePrincipalArn, 'arn:aws:iam::123456789012:role/AdminRole') + assert.strictEqual(result.items[1].id, 'gp_profile2') + assert.strictEqual(result.nextToken, 'next-token') + + // Verify API was called with correct parameters + assert.ok(mockDataZoneClient.searchGroupProfiles.calledOnce) + const callArgs = mockDataZoneClient.searchGroupProfiles.firstCall.args[0] + assert.strictEqual(callArgs.domainIdentifier, mockDomainId) + assert.strictEqual(callArgs.groupType, 'IAM_ROLE_SESSION_GROUP') + assert.strictEqual(callArgs.maxResults, 50) + }) + + it('should handle empty results', async () => { + const mockResponse = { + items: [], + nextToken: undefined, + } + + const mockDataZoneClient = { + searchGroupProfiles: sinon.stub().returns({ + promise: () => Promise.resolve(mockResponse), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + const result = await client.searchGroupProfiles(mockDomainId) + + assert.strictEqual(result.items.length, 0) + assert.strictEqual(result.nextToken, undefined) + }) + + it('should handle API errors', async () => { + const error = new Error('API Error') + const mockDataZoneClient = { + searchGroupProfiles: sinon.stub().returns({ + promise: () => Promise.reject(error), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + await assert.rejects(() => client.searchGroupProfiles(mockDomainId), error) + }) + + it('should support pagination with nextToken', async () => { + const mockResponse = { + items: [ + { + domainId: mockDomainId, + id: 'gp_profile3', + status: 'ACTIVATED', + groupName: 'TestGroup', + rolePrincipalArn: 'arn:aws:iam::123456789012:role/TestRole', + rolePrincipalId: 'AIDAI111111111EXAMPLE', + }, + ], + nextToken: undefined, + } + + const mockDataZoneClient = { + searchGroupProfiles: sinon.stub().returns({ + promise: () => Promise.resolve(mockResponse), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + const result = await client.searchGroupProfiles(mockDomainId, { + nextToken: 'previous-token', + }) + + assert.strictEqual(result.items.length, 1) + assert.strictEqual(result.nextToken, undefined) + + // Verify nextToken was passed + const callArgs = mockDataZoneClient.searchGroupProfiles.firstCall.args[0] + assert.strictEqual(callArgs.nextToken, 'previous-token') + }) + }) + + describe('searchUserProfiles', () => { + const mockDomainId = 'dzd_test123' + + it('should search user profiles successfully', async () => { + const mockResponse = { + items: [ + { + domainId: mockDomainId, + id: 'up_user1', + type: 'IAM', + status: 'ACTIVATED', + details: { + iam: { + arn: 'arn:aws:iam::123456789012:role/AdminRole', + principalId: 'AIDAI123456789EXAMPLE:session1', + }, + }, + }, + { + domainId: mockDomainId, + id: 'up_user2', + type: 'IAM', + status: 'ACTIVATED', + details: { + iam: { + arn: 'arn:aws:iam::123456789012:role/DeveloperRole', + principalId: 'AIDAI987654321EXAMPLE:session2', + }, + }, + }, + ], + nextToken: 'next-token', + } + + const mockDataZoneClient = { + searchUserProfiles: sinon.stub().returns({ + promise: () => Promise.resolve(mockResponse), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + const result = await client.searchUserProfiles(mockDomainId, { + userType: 'DATAZONE_IAM_USER', + maxResults: 50, + }) + + assert.strictEqual(result.items.length, 2) + assert.strictEqual(result.items[0].id, 'up_user1') + assert.strictEqual(result.items[0].details?.iam?.principalId, 'AIDAI123456789EXAMPLE:session1') + assert.strictEqual(result.items[1].id, 'up_user2') + assert.strictEqual(result.nextToken, 'next-token') + + // Verify API was called with correct parameters + assert.ok(mockDataZoneClient.searchUserProfiles.calledOnce) + const callArgs = mockDataZoneClient.searchUserProfiles.firstCall.args[0] + assert.strictEqual(callArgs.domainIdentifier, mockDomainId) + assert.strictEqual(callArgs.userType, 'DATAZONE_IAM_USER') + assert.strictEqual(callArgs.maxResults, 50) + }) + + it('should handle SSO user profiles', async () => { + const mockResponse = { + items: [ + { + domainId: mockDomainId, + id: 'up_sso_user', + type: 'SSO', + status: 'ACTIVATED', + details: { + sso: { + firstName: 'John', + lastName: 'Doe', + username: 'jdoe', + }, + }, + }, + ], + nextToken: undefined, + } + + const mockDataZoneClient = { + searchUserProfiles: sinon.stub().returns({ + promise: () => Promise.resolve(mockResponse), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + const result = await client.searchUserProfiles(mockDomainId, { + userType: 'SSO_USER', + }) + + assert.strictEqual(result.items.length, 1) + assert.strictEqual(result.items[0].details?.sso?.username, 'jdoe') + assert.strictEqual(result.items[0].details?.sso?.firstName, 'John') + }) + + it('should handle empty results', async () => { + const mockResponse = { + items: [], + nextToken: undefined, + } + + const mockDataZoneClient = { + searchUserProfiles: sinon.stub().returns({ + promise: () => Promise.resolve(mockResponse), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + const result = await client.searchUserProfiles(mockDomainId, { + userType: 'DATAZONE_IAM_USER', + }) + + assert.strictEqual(result.items.length, 0) + assert.strictEqual(result.nextToken, undefined) + }) + + it('should handle API errors', async () => { + const error = new Error('API Error') + const mockDataZoneClient = { + searchUserProfiles: sinon.stub().returns({ + promise: () => Promise.reject(error), + }), + } + + sinon.stub(client as any, 'getDataZoneCustomClient').resolves(mockDataZoneClient) + + await assert.rejects( + () => + client.searchUserProfiles(mockDomainId, { + userType: 'DATAZONE_IAM_USER', + }), + error + ) + }) + }) + + describe('getGroupProfileId', () => { + const mockDomainId = 'dzd_test123' + const mockRoleArn = 'arn:aws:iam::123456789012:role/AdminRole' + + it('should find matching group profile on first page', async () => { + const searchStub = sinon.stub(client, 'searchGroupProfiles') + searchStub.onFirstCall().resolves({ + items: [ + { + id: 'gp_profile1', + rolePrincipalArn: mockRoleArn, + status: 'ACTIVATED', + }, + ], + nextToken: undefined, + }) + + const result = await client.getGroupProfileId(mockDomainId, mockRoleArn) + + assert.strictEqual(result, 'gp_profile1') + assert.ok(searchStub.calledOnce) + assert.strictEqual(searchStub.firstCall.args[0], mockDomainId) + assert.strictEqual(searchStub.firstCall.args[1]?.groupType, 'IAM_ROLE_SESSION_GROUP') + }) + + it('should throw ToolkitError when no matching profile found', async () => { + const searchStub = sinon.stub(client, 'searchGroupProfiles') + searchStub.resolves({ + items: [ + { + id: 'gp_profile1', + rolePrincipalArn: 'arn:aws:iam::123456789012:role/OtherRole', + status: 'ACTIVATED', + }, + ], + nextToken: undefined, + }) + + await assert.rejects( + () => client.getGroupProfileId(mockDomainId, mockRoleArn), + (err: any) => { + assert.ok(err.message.includes('No group profile found')) + assert.strictEqual(err.code, 'NoGroupProfileFound') + return true + } + ) + }) + + it('should handle API errors', async () => { + const searchStub = sinon.stub(client, 'searchGroupProfiles') + searchStub.rejects(new Error('API Error')) + + await assert.rejects( + () => client.getGroupProfileId(mockDomainId, mockRoleArn), + (err: any) => { + assert.ok(err.message.includes('Failed to get group profile ID')) + return true + } + ) + }) + }) + + describe('getUserProfileIdForSession', () => { + const mockDomainId = 'dzd_test123' + const mockAssumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/AdminRole/my-session' + + it('should find matching user profile by role ARN and session name', async () => { + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.onFirstCall().resolves({ + items: [ + { + id: 'up_user1', + status: 'ACTIVATED', + details: { + iam: { + arn: 'arn:aws:iam::123456789012:role/AdminRole', + principalId: 'AIDAI123456789EXAMPLE:my-session', + }, + }, + }, + ], + nextToken: undefined, + }) + + const result = await client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn) + + assert.strictEqual(result, 'up_user1') + assert.ok(searchStub.calledOnce) + assert.strictEqual(searchStub.firstCall.args[0], mockDomainId) + assert.strictEqual(searchStub.firstCall.args[1].userType, 'DATAZONE_IAM_USER') + assert.strictEqual(searchStub.firstCall.args[1].searchText, 'arn:aws:iam::123456789012:role/AdminRole') + }) + + it('should find matching user profile across multiple pages', async () => { + const searchStub = sinon.stub(client, 'searchUserProfiles') + + // First page - no match (different session name) + searchStub.onFirstCall().resolves({ + items: [ + { + id: 'up_user1', + status: 'ACTIVATED', + details: { + iam: { + arn: 'arn:aws:iam::123456789012:role/AdminRole', + principalId: 'AIDAI123456789EXAMPLE:other-session', + }, + }, + }, + ], + nextToken: 'next-token', + }) + + // Second page - match found + searchStub.onSecondCall().resolves({ + items: [ + { + id: 'up_user2', + status: 'ACTIVATED', + details: { + iam: { + arn: 'arn:aws:iam::123456789012:role/AdminRole', + principalId: 'AIDAI987654321EXAMPLE:my-session', + }, + }, + }, + ], + nextToken: undefined, + }) + + const result = await client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn) + + assert.strictEqual(result, 'up_user2') + assert.strictEqual(searchStub.callCount, 2) + }) + + it('should throw ToolkitError when session name cannot be extracted', async () => { + const invalidArn = 'arn:aws:iam::123456789012:role/AdminRole' + + await assert.rejects( + () => client.getUserProfileIdForSession(mockDomainId, invalidArn), + (err: any) => { + assert.ok(err.message.includes('Unable to extract session name')) + assert.strictEqual(err.code, 'NoUserProfileFound') + return true + } + ) + }) + + it('should throw ToolkitError when no matching profile found', async () => { + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.resolves({ + items: [ + { + id: 'up_user1', + status: 'ACTIVATED', + details: { + iam: { + arn: 'arn:aws:iam::123456789012:role/AdminRole', + principalId: 'AIDAI123456789EXAMPLE:other-session', + }, + }, + }, + ], + nextToken: undefined, + }) + + await assert.rejects( + () => client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn), + (err: any) => { + assert.ok(err.message.includes('No user profile found')) + assert.strictEqual(err.code, 'NoUserProfileFound') + return true + } + ) + }) + + it('should handle profiles without IAM details', async () => { + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.resolves({ + items: [ + { + id: 'up_user1', + status: 'ACTIVATED', + details: { + // No iam field + }, + }, + ], + nextToken: undefined, + }) + + await assert.rejects( + () => client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn), + (err: any) => { + assert.ok(err.message.includes('No user profile found')) + return true + } + ) + }) + + it('should handle API errors', async () => { + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.rejects(new Error('API Error')) + + await assert.rejects( + () => client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn), + (err: any) => { + assert.ok(err.message.includes('Failed to get user profile ID')) + return true + } + ) + }) + + it('should handle various role ARN formats', async () => { + const testCases = [ + { + arn: 'arn:aws:sts::123456789012:assumed-role/MyRole/session-123', + expectedSession: 'session-123', + }, + { + arn: 'arn:aws:sts::123456789012:assumed-role/DeveloperRole/user-session-name', + expectedSession: 'user-session-name', + }, + { + arn: 'arn:aws:sts::999888777666:assumed-role/AdminRole/admin-session', + expectedSession: 'admin-session', + }, + ] + + for (const testCase of testCases) { + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.resolves({ + items: [ + { + id: 'up_test', + status: 'ACTIVATED', + details: { + iam: { + principalId: `PRINCIPAL:${testCase.expectedSession}`, + }, + }, + }, + ], + nextToken: undefined, + }) + + const result = await client.getUserProfileIdForSession(mockDomainId, testCase.arn) + assert.strictEqual(result, 'up_test') + + searchStub.restore() + } + }) + }) + + describe('Project and Space Filtering', () => { + const mockDomainId = 'dzd_test123' + + describe('Project filtering by group profile', () => { + it('should filter projects when group profile is found', async () => { + const mockRoleArn = 'arn:aws:iam::123456789012:role/AdminRole' + const mockGroupProfileId = 'gp_profile1' + + const searchStub = sinon.stub(client, 'searchGroupProfiles') + searchStub.resolves({ + items: [ + { + id: mockGroupProfileId, + rolePrincipalArn: mockRoleArn, + status: 'ACTIVATED', + }, + ], + nextToken: undefined, + }) + + const result = await client.getGroupProfileId(mockDomainId, mockRoleArn) + + assert.strictEqual(result, mockGroupProfileId) + assert.ok(searchStub.calledOnce) + }) + + it('should handle empty project list for group profile', async () => { + const mockRoleArn = 'arn:aws:iam::123456789012:role/AdminRole' + + const searchStub = sinon.stub(client, 'searchGroupProfiles') + searchStub.resolves({ + items: [], + nextToken: undefined, + }) + + await assert.rejects( + () => client.getGroupProfileId(mockDomainId, mockRoleArn), + (err: any) => { + assert.ok(err.message.includes('No group profile found')) + assert.strictEqual(err.code, 'NoGroupProfileFound') + return true + } + ) + }) + + it('should handle API errors during project filtering', async () => { + const mockRoleArn = 'arn:aws:iam::123456789012:role/AdminRole' + + const searchStub = sinon.stub(client, 'searchGroupProfiles') + searchStub.rejects(new Error('API Error')) + + await assert.rejects( + () => client.getGroupProfileId(mockDomainId, mockRoleArn), + (err: any) => { + assert.ok(err.message.includes('Failed to get group profile ID')) + return true + } + ) + }) + + it('should handle AccessDeniedException during project filtering', async () => { + const mockRoleArn = 'arn:aws:iam::123456789012:role/AdminRole' + const accessDeniedError = new Error('Access denied') + accessDeniedError.name = 'AccessDeniedException' + + const searchStub = sinon.stub(client, 'searchGroupProfiles') + searchStub.rejects(accessDeniedError) + + await assert.rejects( + () => client.getGroupProfileId(mockDomainId, mockRoleArn), + (err: any) => { + assert.ok(err.message.includes('Failed to get group profile ID')) + return true + } + ) + }) + }) + + describe('Space filtering by user profile', () => { + it('should filter spaces when user profile is found', async () => { + const mockAssumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/AdminRole/my-session' + const mockUserProfileId = 'up_user1' + + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.resolves({ + items: [ + { + id: mockUserProfileId, + status: 'ACTIVATED', + details: { + iam: { + arn: 'arn:aws:iam::123456789012:role/AdminRole', + principalId: 'AIDAI123456789EXAMPLE:my-session', + }, + }, + }, + ], + nextToken: undefined, + }) + + const result = await client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn) + + assert.strictEqual(result, mockUserProfileId) + assert.ok(searchStub.calledOnce) + }) + + it('should handle empty space list for user profile', async () => { + const mockAssumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/AdminRole/my-session' + + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.resolves({ + items: [], + nextToken: undefined, + }) + + await assert.rejects( + () => client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn), + (err: any) => { + assert.ok(err.message.includes('No user profile found')) + assert.strictEqual(err.code, 'NoUserProfileFound') + return true + } + ) + }) + + it('should handle API errors during space filtering', async () => { + const mockAssumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/AdminRole/my-session' + + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.rejects(new Error('API Error')) + + await assert.rejects( + () => client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn), + (err: any) => { + assert.ok(err.message.includes('Failed to get user profile ID')) + return true + } + ) + }) + + it('should handle AccessDeniedException during space filtering', async () => { + const mockAssumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/AdminRole/my-session' + const accessDeniedError = new Error('Access denied') + accessDeniedError.name = 'AccessDeniedException' + + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.rejects(accessDeniedError) + + await assert.rejects( + () => client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn), + (err: any) => { + assert.ok(err.message.includes('Failed to get user profile ID')) + return true + } + ) + }) + + it('should handle profiles with missing principalId', async () => { + const mockAssumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/AdminRole/my-session' + + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.resolves({ + items: [ + { + id: 'up_user1', + status: 'ACTIVATED', + details: { + iam: { + // Missing principalId + }, + }, + }, + ], + nextToken: undefined, + }) + + await assert.rejects( + () => client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn), + (err: any) => { + assert.ok(err.message.includes('No user profile found')) + return true + } + ) + }) + }) + + describe('Error scenarios in filtering logic', () => { + it('should handle network errors during group profile search', async () => { + const mockRoleArn = 'arn:aws:iam::123456789012:role/AdminRole' + const networkError = new Error('Network error') + networkError.name = 'NetworkError' + + const searchStub = sinon.stub(client, 'searchGroupProfiles') + searchStub.rejects(networkError) + + await assert.rejects( + () => client.getGroupProfileId(mockDomainId, mockRoleArn), + (err: any) => { + assert.ok(err.message.includes('Failed to get group profile ID')) + return true + } + ) + }) + + it('should handle network errors during user profile search', async () => { + const mockAssumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/AdminRole/my-session' + const networkError = new Error('Network error') + networkError.name = 'NetworkError' + + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.rejects(networkError) + + await assert.rejects( + () => client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn), + (err: any) => { + assert.ok(err.message.includes('Failed to get user profile ID')) + return true + } + ) + }) + + it('should handle timeout errors during group profile search', async () => { + const mockRoleArn = 'arn:aws:iam::123456789012:role/AdminRole' + const timeoutError = new Error('Request timeout') + timeoutError.name = 'TimeoutError' + + const searchStub = sinon.stub(client, 'searchGroupProfiles') + searchStub.rejects(timeoutError) + + await assert.rejects( + () => client.getGroupProfileId(mockDomainId, mockRoleArn), + (err: any) => { + assert.ok(err.message.includes('Failed to get group profile ID')) + return true + } + ) + }) + + it('should handle malformed response during group profile search', async () => { + const mockRoleArn = 'arn:aws:iam::123456789012:role/AdminRole' + + const searchStub = sinon.stub(client, 'searchGroupProfiles') + searchStub.resolves({ + items: [ + { + // Missing required fields + status: 'ACTIVATED', + } as any, + ], + nextToken: undefined, + }) + + await assert.rejects( + () => client.getGroupProfileId(mockDomainId, mockRoleArn), + (err: any) => { + assert.ok(err.message.includes('No group profile found')) + return true + } + ) + }) + + it('should handle malformed response during user profile search', async () => { + const mockAssumedRoleArn = 'arn:aws:sts::123456789012:assumed-role/AdminRole/my-session' + + const searchStub = sinon.stub(client, 'searchUserProfiles') + searchStub.resolves({ + items: [ + { + // Missing required fields + status: 'ACTIVATED', + } as any, + ], + nextToken: undefined, + }) + + await assert.rejects( + () => client.getUserProfileIdForSession(mockDomainId, mockAssumedRoleArn), + (err: any) => { + assert.ok(err.message.includes('No user profile found')) + return true + } + ) + }) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/shared/client/glueClient.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/client/glueClient.test.ts new file mode 100644 index 00000000000..cd34fe7703e --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/client/glueClient.test.ts @@ -0,0 +1,202 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { GlueClient } from '../../../../sagemakerunifiedstudio/shared/client/glueClient' +import { Glue, GetDatabasesCommand, GetTablesCommand, GetTableCommand } from '@aws-sdk/client-glue' +import { ConnectionCredentialsProvider } from '../../../../sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider' + +describe('GlueClient', function () { + let sandbox: sinon.SinonSandbox + let glueClient: GlueClient + let mockGlue: sinon.SinonStubbedInstance + + const mockCredentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + + beforeEach(function () { + sandbox = sinon.createSandbox() + + mockGlue = { + send: sandbox.stub(), + } as any + + sandbox.stub(Glue.prototype, 'send').callsFake(mockGlue.send) + + glueClient = new GlueClient('us-east-1', mockCredentialsProvider as ConnectionCredentialsProvider) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('getDatabases', function () { + it('should get databases successfully', async function () { + const mockResponse = { + DatabaseList: [ + { Name: 'database1', Description: 'Test database 1' }, + { Name: 'database2', Description: 'Test database 2' }, + ], + NextToken: 'next-token', + } + + mockGlue.send.resolves(mockResponse) + + const result = await glueClient.getDatabases('test-catalog', undefined, undefined, 'start-token') + + assert.strictEqual(result.databases.length, 2) + assert.strictEqual(result.databases[0].Name, 'database1') + assert.strictEqual(result.nextToken, 'next-token') + + const sendCall = mockGlue.send.getCall(0) + const command = sendCall.args[0] as GetDatabasesCommand + assert.ok(command instanceof GetDatabasesCommand) + }) + + it('should get databases without catalog ID', async function () { + const mockResponse = { + DatabaseList: [{ Name: 'default-db' }], + } + + mockGlue.send.resolves(mockResponse) + + const result = await glueClient.getDatabases() + + assert.strictEqual(result.databases.length, 1) + assert.strictEqual(result.databases[0].Name, 'default-db') + assert.strictEqual(result.nextToken, undefined) + }) + + it('should handle errors when getting databases', async function () { + const error = new Error('Access denied') + mockGlue.send.rejects(error) + + await assert.rejects( + async () => { + await glueClient.getDatabases('test-catalog') + }, + { + message: 'Access denied', + } + ) + }) + }) + + describe('getTables', function () { + it('should get tables successfully', async function () { + const mockResponse = { + TableList: [ + { Name: 'table1', DatabaseName: 'test-db' }, + { Name: 'table2', DatabaseName: 'test-db' }, + ], + NextToken: 'next-token', + } + + mockGlue.send.resolves(mockResponse) + + const result = await glueClient.getTables('test-db', 'test-catalog', undefined, 'start-token') + + assert.strictEqual(result.tables.length, 2) + assert.strictEqual(result.tables[0].Name, 'table1') + assert.strictEqual(result.nextToken, 'next-token') + + const sendCall = mockGlue.send.getCall(0) + const command = sendCall.args[0] as GetTablesCommand + assert.ok(command instanceof GetTablesCommand) + }) + + it('should get tables without catalog ID', async function () { + const mockResponse = { + TableList: [{ Name: 'default-table' }], + } + + mockGlue.send.resolves(mockResponse) + + const result = await glueClient.getTables('test-db') + + assert.strictEqual(result.tables.length, 1) + assert.strictEqual(result.tables[0].Name, 'default-table') + }) + + it('should handle errors when getting tables', async function () { + const error = new Error('Database not found') + mockGlue.send.rejects(error) + + await assert.rejects( + async () => { + await glueClient.getTables('nonexistent-db') + }, + { + message: 'Database not found', + } + ) + }) + }) + + describe('getTable', function () { + it('should get table details successfully', async function () { + const mockResponse = { + Table: { + Name: 'test-table', + DatabaseName: 'test-db', + StorageDescriptor: { + Columns: [ + { Name: 'col1', Type: 'string' }, + { Name: 'col2', Type: 'int' }, + ], + }, + PartitionKeys: [{ Name: 'partition_col', Type: 'date' }], + }, + } + + mockGlue.send.resolves(mockResponse) + + const result = await glueClient.getTable('test-db', 'test-table', 'test-catalog') + + assert.strictEqual(result?.Name, 'test-table') + assert.strictEqual(result?.StorageDescriptor?.Columns?.length, 2) + assert.strictEqual(result?.PartitionKeys?.length, 1) + + const sendCall = mockGlue.send.getCall(0) + const command = sendCall.args[0] as GetTableCommand + assert.ok(command instanceof GetTableCommand) + }) + + it('should get table without catalog ID', async function () { + const mockResponse = { + Table: { + Name: 'default-table', + DatabaseName: 'default-db', + }, + } + + mockGlue.send.resolves(mockResponse) + + const result = await glueClient.getTable('default-db', 'default-table') + + assert.strictEqual(result?.Name, 'default-table') + }) + + it('should handle errors when getting table', async function () { + const error = new Error('Table not found') + mockGlue.send.rejects(error) + + await assert.rejects( + async () => { + await glueClient.getTable('test-db', 'nonexistent-table') + }, + { + message: 'Table not found', + } + ) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/shared/client/gluePrivateClient.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/client/gluePrivateClient.test.ts new file mode 100644 index 00000000000..049d9e256d0 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/client/gluePrivateClient.test.ts @@ -0,0 +1,150 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { GlueCatalogClient } from '../../../../sagemakerunifiedstudio/shared/client/glueCatalogClient' +import { ConnectionCredentialsProvider } from '../../../../sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider' +import { GlueCatalog } from '@amzn/glue-catalog-client' + +describe('GlueCatalogClient', function () { + let sandbox: sinon.SinonSandbox + let mockGlueCatalogService: any + let glueCatalogConstructorStub: sinon.SinonStub + + beforeEach(function () { + sandbox = sinon.createSandbox() + + mockGlueCatalogService = { + getCatalogs: sandbox.stub().resolves({ + CatalogList: [ + { + Name: 'test-catalog', + CatalogType: 'HIVE', + Parameters: { key1: 'value1' }, + }, + ], + }), + } + + // Stub the GlueCatalog constructor + glueCatalogConstructorStub = sandbox.stub(GlueCatalog.prototype, 'getCatalogs') + glueCatalogConstructorStub.callsFake(mockGlueCatalogService.getCatalogs) + }) + + afterEach(function () { + sandbox.restore() + // Reset singleton instance + ;(GlueCatalogClient as any).instance = undefined + }) + + describe('getInstance', function () { + it('should create singleton instance', function () { + const client1 = GlueCatalogClient.getInstance('us-east-1') + const client2 = GlueCatalogClient.getInstance('us-east-1') + + assert.strictEqual(client1, client2) + }) + + it('should return region correctly', function () { + const client = GlueCatalogClient.getInstance('us-west-2') + assert.strictEqual(client.getRegion(), 'us-west-2') + }) + }) + + describe('createWithCredentials', function () { + it('should create client with credentials', function () { + const credentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + + const client = GlueCatalogClient.createWithCredentials( + 'us-east-1', + credentialsProvider as ConnectionCredentialsProvider + ) + assert.strictEqual(client.getRegion(), 'us-east-1') + }) + }) + + describe('getCatalogs', function () { + it('should return catalogs successfully', async function () { + const client = GlueCatalogClient.getInstance('us-east-1') + const catalogs = await client.getCatalogs() + + assert.strictEqual(catalogs.catalogs.length, 1) + assert.strictEqual(catalogs.catalogs[0].Name, 'test-catalog') + assert.strictEqual(catalogs.catalogs[0].CatalogType, 'HIVE') + assert.deepStrictEqual(catalogs.catalogs[0].Parameters, { key1: 'value1' }) + }) + + it('should return empty array when no catalogs found', async function () { + mockGlueCatalogService.getCatalogs.resolves({ CatalogList: [] }) + + const client = GlueCatalogClient.getInstance('us-east-1') + const catalogs = await client.getCatalogs() + + assert.strictEqual(catalogs.catalogs.length, 0) + }) + + it('should handle API errors', async function () { + const error = new Error('API Error') + mockGlueCatalogService.getCatalogs.rejects(error) + + const client = GlueCatalogClient.getInstance('us-east-1') + + await assert.rejects(async () => await client.getCatalogs(), error) + }) + + it('should create client with credentials when provided', async function () { + const credentialsProvider = { + getCredentials: sandbox.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + expiration: new Date('2025-12-31'), + }), + } as any + + const client = GlueCatalogClient.createWithCredentials('us-east-1', credentialsProvider) + const result = await client.getCatalogs() + + // Verify the API method was called and returned expected results + assert.ok(glueCatalogConstructorStub.called) + assert.strictEqual(result.catalogs.length, 1) + assert.strictEqual(client.getRegion(), 'us-east-1') + }) + + it('should handle errors when creating client with credentials', async function () { + const credentialsProvider = { + getCredentials: sandbox.stub().resolves({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } as any + + const client = GlueCatalogClient.createWithCredentials('us-east-1', credentialsProvider) + + // Make getCatalogs fail + const error = new Error('Credentials error') + mockGlueCatalogService.getCatalogs.rejects(error) + + await assert.rejects(async () => await client.getCatalogs(), error) + }) + + it('should create client without credentials when not provided', async function () { + const client = GlueCatalogClient.getInstance('us-east-1') + const result = await client.getCatalogs() + + // Verify the method was called + assert.ok(glueCatalogConstructorStub.called) + assert.strictEqual(result.catalogs.length, 1) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/shared/client/s3Client.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/client/s3Client.test.ts new file mode 100644 index 00000000000..714ced3d446 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/client/s3Client.test.ts @@ -0,0 +1,306 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { S3Client } from '../../../../sagemakerunifiedstudio/shared/client/s3Client' +import { S3 } from '@aws-sdk/client-s3' +import { ConnectionCredentialsProvider } from '../../../../sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider' + +describe('S3Client', function () { + let sandbox: sinon.SinonSandbox + let mockS3: sinon.SinonStubbedInstance + let s3Client: S3Client + + const mockCredentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + + beforeEach(function () { + sandbox = sinon.createSandbox() + + mockS3 = { + listObjectsV2: sandbox.stub(), + } as any + + sandbox.stub(S3.prototype, 'constructor' as any) + sandbox.stub(S3.prototype, 'listObjectsV2').callsFake(mockS3.listObjectsV2) + + s3Client = new S3Client('us-east-1', mockCredentialsProvider as ConnectionCredentialsProvider) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('constructor', function () { + it('should create client with correct properties', function () { + const client = new S3Client('us-west-2', mockCredentialsProvider as ConnectionCredentialsProvider) + assert.ok(client) + }) + }) + + describe('listPaths', function () { + it('should list folders and files successfully', async function () { + const mockResponse = { + CommonPrefixes: [{ Prefix: 'folder1/' }, { Prefix: 'folder2/' }], + Contents: [ + { + Key: 'file1.txt', + Size: 1024, + LastModified: new Date('2023-01-01'), + }, + { + Key: 'file2.txt', + Size: 2048, + LastModified: new Date('2023-01-02'), + }, + ], + } + + mockS3.listObjectsV2.resolves(mockResponse) + + const result = await s3Client.listPaths('test-bucket') + + assert.strictEqual(result.paths.length, 4) + const paths = result.paths + + // Check folders + assert.strictEqual(paths[0].displayName, 'folder1') + assert.strictEqual(paths[0].isFolder, true) + assert.strictEqual(paths[0].bucket, 'test-bucket') + assert.strictEqual(paths[0].prefix, 'folder1/') + + assert.strictEqual(paths[1].displayName, 'folder2') + assert.strictEqual(paths[1].isFolder, true) + + // Check files + assert.strictEqual(paths[2].displayName, 'file1.txt') + assert.strictEqual(paths[2].isFolder, false) + assert.strictEqual(paths[2].size, 1024) + assert.deepStrictEqual(paths[2].lastModified, new Date('2023-01-01')) + + assert.strictEqual(paths[3].displayName, 'file2.txt') + assert.strictEqual(paths[3].isFolder, false) + assert.strictEqual(paths[3].size, 2048) + }) + + it('should list paths with prefix', async function () { + const mockResponse = { + CommonPrefixes: [{ Prefix: 'prefix/subfolder/' }], + Contents: [ + { + Key: 'prefix/file.txt', + Size: 512, + LastModified: new Date('2023-01-01'), + }, + ], + } + + mockS3.listObjectsV2.resolves(mockResponse) + + const result = await s3Client.listPaths('test-bucket', 'prefix/') + + assert.strictEqual(result.paths.length, 2) + const paths = result.paths + assert.strictEqual(paths[0].displayName, 'subfolder') + assert.strictEqual(paths[0].isFolder, true) + assert.strictEqual(paths[1].displayName, 'file.txt') + assert.strictEqual(paths[1].isFolder, false) + + // Verify API call + assert.ok(mockS3.listObjectsV2.calledOnce) + const callArgs = mockS3.listObjectsV2.getCall(0).args[0] + assert.strictEqual(callArgs.Bucket, 'test-bucket') + assert.strictEqual(callArgs.Prefix, 'prefix/') + assert.strictEqual(callArgs.Delimiter, '/') + assert.strictEqual(callArgs.ContinuationToken, undefined) + }) + + it('should return empty array when no objects found', async function () { + const mockResponse = { + CommonPrefixes: [], + Contents: [], + } + + mockS3.listObjectsV2.resolves(mockResponse) + + const result = await s3Client.listPaths('empty-bucket') + + assert.strictEqual(result.paths.length, 0) + }) + + it('should handle response with only folders', async function () { + const mockResponse = { + CommonPrefixes: [{ Prefix: 'folder1/' }, { Prefix: 'folder2/' }], + Contents: undefined, + } + + mockS3.listObjectsV2.resolves(mockResponse) + + const result = await s3Client.listPaths('test-bucket') + + assert.strictEqual(result.paths.length, 2) + const paths = result.paths + assert.strictEqual(paths[0].isFolder, true) + assert.strictEqual(paths[1].isFolder, true) + }) + + it('should handle response with only files', async function () { + const mockResponse = { + CommonPrefixes: undefined, + Contents: [ + { + Key: 'file1.txt', + Size: 1024, + LastModified: new Date('2023-01-01'), + }, + ], + } + + mockS3.listObjectsV2.resolves(mockResponse) + + const result = await s3Client.listPaths('test-bucket') + + assert.strictEqual(result.paths.length, 1) + const paths = result.paths + assert.strictEqual(paths[0].isFolder, false) + assert.strictEqual(paths[0].displayName, 'file1.txt') + }) + + it('should filter out folder markers and prefix matches', async function () { + const mockResponse = { + CommonPrefixes: [{ Prefix: 'folder/' }], + Contents: [ + { + Key: 'prefix/', + Size: 0, + LastModified: new Date('2023-01-01'), + }, + { + Key: 'prefix/file.txt', + Size: 1024, + LastModified: new Date('2023-01-01'), + }, + { + Key: 'prefix/folder/', + Size: 0, + LastModified: new Date('2023-01-01'), + }, + ], + } + + mockS3.listObjectsV2.resolves(mockResponse) + + const result = await s3Client.listPaths('test-bucket', 'prefix/') + + // Should only include the folder from CommonPrefixes and the file (not folder markers) + assert.strictEqual(result.paths.length, 2) + const paths = result.paths + assert.strictEqual(paths[0].displayName, 'folder') + assert.strictEqual(paths[0].isFolder, true) + assert.strictEqual(paths[1].displayName, 'file.txt') + assert.strictEqual(paths[1].isFolder, false) + }) + + it('should handle API errors', async function () { + const error = new Error('S3 API Error') + mockS3.listObjectsV2.rejects(error) + + await assert.rejects(async () => await s3Client.listPaths('test-bucket'), error) + }) + + it('should handle missing object properties gracefully', async function () { + const mockResponse = { + CommonPrefixes: [{ Prefix: undefined }, { Prefix: 'valid-folder/' }], + Contents: [ + { + Key: undefined, + Size: 1024, + }, + { + Key: 'valid-file.txt', + Size: undefined, + LastModified: undefined, + }, + ], + } + + mockS3.listObjectsV2.resolves(mockResponse) + + const result = await s3Client.listPaths('test-bucket') + + // Should only include valid entries + assert.strictEqual(result.paths.length, 2) + const paths = result.paths + assert.strictEqual(paths[0].displayName, 'valid-folder') + assert.strictEqual(paths[0].isFolder, true) + assert.strictEqual(paths[1].displayName, 'valid-file.txt') + assert.strictEqual(paths[1].isFolder, false) + assert.strictEqual(paths[1].size, undefined) + assert.strictEqual(paths[1].lastModified, undefined) + }) + + it('should create S3 client on first use', async function () { + const mockResponse = { CommonPrefixes: [], Contents: [] } + mockS3.listObjectsV2.resolves(mockResponse) + + await s3Client.listPaths('test-bucket') + + // Verify S3 client was created with correct parameters + assert.ok(S3.prototype.constructor) + }) + + it('should reuse existing S3 client on subsequent calls', async function () { + const mockResponse = { CommonPrefixes: [], Contents: [] } + mockS3.listObjectsV2.resolves(mockResponse) + + // Make multiple calls + await s3Client.listPaths('test-bucket') + await s3Client.listPaths('test-bucket') + + // S3 constructor should only be called once (during first call) + assert.ok(mockS3.listObjectsV2.calledTwice) + }) + + it('should handle ContinuationToken for pagination', async function () { + const mockResponse = { + CommonPrefixes: [{ Prefix: 'folder1/' }], + Contents: [{ Key: 'file1.txt', Size: 1024 }], + NextContinuationToken: 'next-token-123', + } + + mockS3.listObjectsV2.resolves(mockResponse) + + const result = await s3Client.listPaths('test-bucket', 'prefix/', 'continuation-token') + + assert.strictEqual(result.paths.length, 2) + assert.strictEqual(result.nextToken, 'next-token-123') + + // Verify ContinuationToken was passed + const callArgs = mockS3.listObjectsV2.getCall(0).args[0] + assert.strictEqual(callArgs.ContinuationToken, 'continuation-token') + }) + + it('should return undefined nextToken when no more pages', async function () { + const mockResponse = { + CommonPrefixes: [{ Prefix: 'folder1/' }], + Contents: [{ Key: 'file1.txt', Size: 1024 }], + NextContinuationToken: undefined, + } + + mockS3.listObjectsV2.resolves(mockResponse) + + const result = await s3Client.listPaths('test-bucket') + + assert.strictEqual(result.paths.length, 2) + assert.strictEqual(result.nextToken, undefined) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/shared/client/sqlWorkbenchClient.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/client/sqlWorkbenchClient.test.ts new file mode 100644 index 00000000000..e4b1dc50a85 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/client/sqlWorkbenchClient.test.ts @@ -0,0 +1,249 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { + SQLWorkbenchClient, + generateSqlWorkbenchArn, + createRedshiftConnectionConfig, +} from '../../../../sagemakerunifiedstudio/shared/client/sqlWorkbenchClient' +import { STSClient } from '@aws-sdk/client-sts' +import globals from '../../../../shared/extensionGlobals' +import { ConnectionCredentialsProvider } from '../../../../sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider' + +describe('SQLWorkbenchClient', function () { + let sandbox: sinon.SinonSandbox + let mockSqlClient: any + let mockSdkClientBuilder: any + + beforeEach(function () { + sandbox = sinon.createSandbox() + + mockSqlClient = { + getResources: sandbox.stub().returns({ + promise: sandbox.stub().resolves({ + resources: [{ name: 'test-resource' }], + nextToken: 'next-token', + }), + }), + executeQuery: sandbox.stub().returns({ + promise: sandbox.stub().resolves({ + queryExecutions: [{ queryExecutionId: 'test-execution-id' }], + }), + }), + } + + mockSdkClientBuilder = { + createAwsService: sandbox.stub().resolves(mockSqlClient), + } + + sandbox.stub(globals, 'sdkClientBuilder').value(mockSdkClientBuilder) + }) + + afterEach(function () { + sandbox.restore() + // Reset singleton instance + ;(SQLWorkbenchClient as any).instance = undefined + }) + + describe('getInstance', function () { + it('should create singleton instance', function () { + const client1 = SQLWorkbenchClient.getInstance('us-east-1') + const client2 = SQLWorkbenchClient.getInstance('us-east-1') + + assert.strictEqual(client1, client2) + }) + + it('should return region correctly', function () { + const client = SQLWorkbenchClient.getInstance('us-west-2') + assert.strictEqual(client.getRegion(), 'us-west-2') + }) + }) + + describe('createWithCredentials', function () { + it('should create client with credentials', function () { + const credentialsProvider = { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + + const client = SQLWorkbenchClient.createWithCredentials( + 'us-east-1', + credentialsProvider as ConnectionCredentialsProvider + ) + assert.strictEqual(client.getRegion(), 'us-east-1') + }) + }) + + describe('getResources', function () { + it('should get resources with connection', async function () { + const client = SQLWorkbenchClient.getInstance('us-east-1') + const connectionConfig = { + id: 'test-id', + type: 'test-type', + databaseType: 'REDSHIFT', + connectableResourceIdentifier: 'test-identifier', + connectableResourceType: 'CLUSTER', + database: 'test-db', + } + + const result = await client.getResources({ + connection: connectionConfig, + resourceType: 'TABLE', + maxItems: 50, + }) + + assert.deepStrictEqual(result.resources, [{ name: 'test-resource' }]) + assert.strictEqual(result.nextToken, 'next-token') + }) + + it('should handle API errors', async function () { + const error = new Error('API Error') + mockSqlClient.getResources.returns({ + promise: sandbox.stub().rejects(error), + }) + + const client = SQLWorkbenchClient.getInstance('us-east-1') + + await assert.rejects( + async () => + await client.getResources({ + connection: { + id: '', + type: '', + databaseType: '', + connectableResourceIdentifier: '', + connectableResourceType: '', + database: '', + }, + resourceType: '', + }), + error + ) + }) + }) + + describe('executeQuery', function () { + it('should execute query successfully', async function () { + const client = SQLWorkbenchClient.getInstance('us-east-1') + const connectionConfig = { + id: 'test-id', + type: 'test-type', + databaseType: 'REDSHIFT', + connectableResourceIdentifier: 'test-identifier', + connectableResourceType: 'CLUSTER', + database: 'test-db', + } + + const result = await client.executeQuery(connectionConfig, 'SELECT 1') + + assert.strictEqual(result, 'test-execution-id') + }) + + it('should handle query execution errors', async function () { + const error = new Error('Query Error') + mockSqlClient.executeQuery.returns({ + promise: sandbox.stub().rejects(error), + }) + + const client = SQLWorkbenchClient.getInstance('us-east-1') + const connectionConfig = { + id: 'test-id', + type: 'test-type', + databaseType: 'REDSHIFT', + connectableResourceIdentifier: 'test-identifier', + connectableResourceType: 'CLUSTER', + database: 'test-db', + } + + await assert.rejects(async () => await client.executeQuery(connectionConfig, 'SELECT 1'), error) + }) + }) +}) + +describe('generateSqlWorkbenchArn', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('should generate ARN with provided account ID', async function () { + const arn = await generateSqlWorkbenchArn('us-east-1', '123456789012') + + assert.ok(arn.startsWith('arn:aws:sqlworkbench:us-east-1:123456789012:connection/')) + assert.ok(arn.includes('-')) + }) +}) + +describe('createRedshiftConnectionConfig', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + sandbox.stub(STSClient.prototype, 'send').resolves({ Account: '123456789012' }) + }) + + afterEach(function () { + sandbox.restore() + }) + + it('should create serverless connection config', async function () { + const config = await createRedshiftConnectionConfig( + 'test-workgroup.123456789012.us-east-1.redshift-serverless.amazonaws.com', + 'test-db', + '123456789012', + 'us-east-1', + '', + false + ) + + assert.strictEqual(config.databaseType, 'REDSHIFT') + assert.strictEqual(config.connectableResourceType, 'WORKGROUP') + assert.strictEqual(config.connectableResourceIdentifier, 'test-workgroup') + assert.strictEqual(config.database, 'test-db') + assert.strictEqual(config.type, '4') // FEDERATED + }) + + it('should create cluster connection config', async function () { + const config = await createRedshiftConnectionConfig( + 'test-cluster.123456789012.us-east-1.redshift.amazonaws.com', + 'test-db', + '123456789012', + 'us-east-1', + '', + false + ) + + assert.strictEqual(config.databaseType, 'REDSHIFT') + assert.strictEqual(config.connectableResourceType, 'CLUSTER') + assert.strictEqual(config.connectableResourceIdentifier, 'test-cluster') + assert.strictEqual(config.database, 'test-db') + assert.strictEqual(config.type, '5') // TEMPORARY_CREDENTIALS_WITH_IAM + }) + + it('should create config with secret authentication', async function () { + const config = await createRedshiftConnectionConfig( + 'test-cluster.123456789012.us-east-1.redshift.amazonaws.com', + 'test-db', + '123456789012', + 'us-east-1', + 'arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret', + false + ) + + assert.strictEqual(config.type, '6') // SECRET + assert.ok(config.auth) + assert.strictEqual(config.auth.secretArn, 'arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret') + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/shared/devSettingsEndpointConfiguration.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/devSettingsEndpointConfiguration.test.ts new file mode 100644 index 00000000000..c5321283017 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/devSettingsEndpointConfiguration.test.ts @@ -0,0 +1,86 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { DevSettings } from '../../../shared/settings' + +describe('Endpoint Configuration from Settings', () => { + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('DataZone endpoint configuration', () => { + it('should return custom DataZone endpoint when configured', () => { + const customEndpoint = 'https://custom-datazone.example.com' + const getStub = sandbox.stub(DevSettings.instance, 'get') + getStub.withArgs('endpoints', {}).returns({ datazone: customEndpoint }) + + const endpoints = DevSettings.instance.get('endpoints', {}) + const datazoneEndpoint = endpoints['datazone'] + + assert.strictEqual(datazoneEndpoint, customEndpoint) + }) + }) + + describe('SageMaker endpoint configuration', () => { + it('should return custom SageMaker endpoint when configured', () => { + const customEndpoint = 'https://custom-sagemaker.example.com' + const getStub = sandbox.stub(DevSettings.instance, 'get') + getStub.withArgs('endpoints', {}).returns({ sagemaker: customEndpoint }) + + const endpoints = DevSettings.instance.get('endpoints', {}) + const sagemakerEndpoint = endpoints['sagemaker'] + + assert.strictEqual(sagemakerEndpoint, customEndpoint) + }) + }) + + describe('Endpoint fallback behavior', () => { + it('should construct default DataZone endpoint when custom endpoint is not set', () => { + const getStub = sandbox.stub(DevSettings.instance, 'get') + getStub.withArgs('endpoints', {}).returns({}) + + const region = 'us-west-2' + const endpoints = DevSettings.instance.get('endpoints', {}) + const customEndpoint = endpoints['datazone'] + const endpoint = customEndpoint || `https://datazone.${region}.api.aws` + + assert.strictEqual(endpoint, 'https://datazone.us-west-2.api.aws') + }) + + it('should construct default SageMaker endpoint when custom endpoint is not set', () => { + const getStub = sandbox.stub(DevSettings.instance, 'get') + getStub.withArgs('endpoints', {}).returns({}) + + const region = 'us-east-1' + const endpoints = DevSettings.instance.get('endpoints', {}) + const customEndpoint = endpoints['sagemaker'] + const endpoint = customEndpoint || `https://sagemaker.${region}.amazonaws.com` + + assert.strictEqual(endpoint, 'https://sagemaker.us-east-1.amazonaws.com') + }) + + it('should handle multiple endpoints in configuration', () => { + const customEndpoints = { + datazone: 'https://custom-datazone.example.com', + sagemaker: 'https://custom-sagemaker.example.com', + } + const getStub = sandbox.stub(DevSettings.instance, 'get') + getStub.withArgs('endpoints', {}).returns(customEndpoints) + + const endpoints = DevSettings.instance.get('endpoints', {}) + + assert.strictEqual(endpoints['datazone'], customEndpoints.datazone) + assert.strictEqual(endpoints['sagemaker'], customEndpoints.sagemaker) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/shared/smusUtils.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/smusUtils.test.ts new file mode 100644 index 00000000000..a0b556ca664 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/smusUtils.test.ts @@ -0,0 +1,797 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { + SmusUtils, + SmusErrorCodes, + SmusTimeouts, + SmusCredentialExpiry, + validateCredentialFields, + extractAccountIdFromSageMakerArn, + extractAccountIdFromResourceMetadata, + isCredentialExpirationError, + isIamDomain, + IamSignInRole, + IamSignInUser, + DomainVersionV1, + DomainVersionV2, +} from '../../../sagemakerunifiedstudio/shared/smusUtils' +import { ToolkitError } from '../../../shared/errors' +import * as extensionUtilities from '../../../shared/extensionUtilities' +import * as resourceMetadataUtils from '../../../sagemakerunifiedstudio/shared/utils/resourceMetadataUtils' +import fetch from 'node-fetch' + +describe('SmusUtils', () => { + const testDomainUrl = 'https://dzd_domainId.sagemaker.us-east-2.on.aws' + const testDomainIdLowercase = 'dzd_domainid' // Domain IDs get lowercased by URL parsing + const testRegion = 'us-east-2' + + afterEach(() => { + sinon.restore() + }) + + describe('extractDomainIdFromUrl', () => { + it('should extract domain ID from valid URL', () => { + const result = SmusUtils.extractDomainIdFromUrl(testDomainUrl) + assert.strictEqual(result, testDomainIdLowercase) + }) + + it('should return undefined for invalid URL', () => { + const result = SmusUtils.extractDomainIdFromUrl('invalid-url') + assert.strictEqual(result, undefined) + }) + + it('should handle URLs with dzd- prefix', () => { + const urlWithDash = 'https://dzd-domainId.sagemaker.us-east-2.on.aws' + const result = SmusUtils.extractDomainIdFromUrl(urlWithDash) + assert.strictEqual(result, 'dzd-domainid') + }) + + it('should handle URLs with dzd_ prefix', () => { + const urlWithUnderscore = 'https://dzd_domainId.sagemaker.us-east-2.on.aws' + const result = SmusUtils.extractDomainIdFromUrl(urlWithUnderscore) + assert.strictEqual(result, testDomainIdLowercase) + }) + }) + + describe('extractRegionFromUrl', () => { + it('should extract region from valid URL', () => { + const result = SmusUtils.extractRegionFromUrl(testDomainUrl) + assert.strictEqual(result, testRegion) + }) + + it('should return fallback region for invalid URL', () => { + const result = SmusUtils.extractRegionFromUrl('invalid-url', 'us-west-2') + assert.strictEqual(result, 'us-west-2') + }) + + it('should return default fallback region when not specified', () => { + const result = SmusUtils.extractRegionFromUrl('invalid-url') + assert.strictEqual(result, 'us-east-1') + }) + + it('should handle different regions', () => { + const urlWithDifferentRegion = 'https://dzd_test.sagemaker.eu-west-1.on.aws' + const result = SmusUtils.extractRegionFromUrl(urlWithDifferentRegion) + assert.strictEqual(result, 'eu-west-1') + }) + + it('should handle non-prod stages', () => { + const urlWithStage = 'https://dzd_test.sagemaker-gamma.us-west-2.on.aws' + const result = SmusUtils.extractRegionFromUrl(urlWithStage) + assert.strictEqual(result, 'us-west-2') + }) + }) + + describe('extractDomainInfoFromUrl', () => { + it('should extract both domain ID and region', () => { + const result = SmusUtils.extractDomainInfoFromUrl(testDomainUrl) + assert.strictEqual(result.domainId, testDomainIdLowercase) + assert.strictEqual(result.region, testRegion) + }) + + it('should use fallback region when extraction fails', () => { + const result = SmusUtils.extractDomainInfoFromUrl('invalid-url', 'us-west-2') + assert.strictEqual(result.domainId, undefined) + assert.strictEqual(result.region, 'us-west-2') + }) + }) + + describe('validateDomainUrl', () => { + it('should return undefined for valid URL', () => { + const result = SmusUtils.validateDomainUrl(testDomainUrl) + assert.strictEqual(result, undefined) + }) + + it('should return error for empty URL', () => { + const result = SmusUtils.validateDomainUrl('') + assert.strictEqual(result, 'Domain URL is required') + }) + + it('should return error for whitespace-only URL', () => { + const result = SmusUtils.validateDomainUrl(' ') + assert.strictEqual(result, 'Domain URL is required') + }) + + it('should return error for non-HTTPS URL', () => { + const result = SmusUtils.validateDomainUrl('http://dzd_test.sagemaker.us-east-1.on.aws') + assert.strictEqual(result, 'Domain URL must use HTTPS (https://)') + }) + + it('should return error for non-SageMaker domain', () => { + const result = SmusUtils.validateDomainUrl('https://example.com') + assert.strictEqual( + result, + 'URL must be a valid SageMaker Unified Studio domain (e.g., https://dzd_xxxxxxxxx.sagemaker.us-east-1.on.aws)' + ) + }) + + it('should return error for URL without domain ID', () => { + const result = SmusUtils.validateDomainUrl('https://invalid.sagemaker.us-east-1.on.aws') + assert.strictEqual(result, 'URL must contain a valid domain ID (starting with dzd- or dzd_)') + }) + + it('should return error for invalid URL format', () => { + const result = SmusUtils.validateDomainUrl('not-a-url') + assert.strictEqual(result, 'Domain URL must use HTTPS (https://)') + }) + + it('should handle URLs with dzd- prefix', () => { + const urlWithDash = 'https://dzd-domainId.sagemaker.us-east-2.on.aws' + const result = SmusUtils.validateDomainUrl(urlWithDash) + assert.strictEqual(result, undefined) + }) + + it('should handle URLs with dzd_ prefix', () => { + const urlWithUnderscore = 'https://dzd_domainId.sagemaker.us-east-2.on.aws' + const result = SmusUtils.validateDomainUrl(urlWithUnderscore) + assert.strictEqual(result, undefined) + }) + + it('should trim whitespace from URL', () => { + const urlWithWhitespace = ' https://dzd_domainId.sagemaker.us-east-2.on.aws ' + const result = SmusUtils.validateDomainUrl(urlWithWhitespace) + assert.strictEqual(result, undefined) + }) + }) + + describe('constants', () => { + it('should export SmusErrorCodes with correct values', () => { + assert.strictEqual(SmusErrorCodes.NoActiveConnection, 'NoActiveConnection') + assert.strictEqual(SmusErrorCodes.ApiTimeout, 'ApiTimeout') + assert.strictEqual(SmusErrorCodes.SmusLoginFailed, 'SmusLoginFailed') + assert.strictEqual(SmusErrorCodes.RedeemAccessTokenFailed, 'RedeemAccessTokenFailed') + }) + + it('should export SmusTimeouts with correct values', () => { + assert.strictEqual(SmusTimeouts.apiCallTimeoutMs, 10 * 1000) + }) + + it('should export SmusCredentialExpiry with correct values', () => { + assert.strictEqual(SmusCredentialExpiry.derExpiryMs, 10 * 60 * 1000) + assert.strictEqual(SmusCredentialExpiry.projectExpiryMs, 10 * 60 * 1000) + assert.strictEqual(SmusCredentialExpiry.connectionExpiryMs, 10 * 60 * 1000) + }) + }) + + describe('getSsoInstanceInfo', () => { + let fetchStub: sinon.SinonStub + + beforeEach(() => { + fetchStub = sinon.stub(fetch, 'default' as any) + }) + + afterEach(() => { + fetchStub.restore() + }) + + it('should throw error for invalid domain URL', async () => { + await assert.rejects( + () => SmusUtils.getSsoInstanceInfo('invalid-url'), + (error: any) => { + assert.strictEqual(error.code, 'InvalidDomainUrl') + return true + } + ) + }) + + it('should throw error for URL without domain ID', async () => { + await assert.rejects( + () => SmusUtils.getSsoInstanceInfo('https://invalid.sagemaker.us-east-1.on.aws'), + (error: any) => { + assert.strictEqual(error.code, 'InvalidDomainUrl') + return true + } + ) + }) + + it('should handle timeout errors', async () => { + const timeoutError = new Error('Request timeout') + timeoutError.name = 'AbortError' + fetchStub.rejects(timeoutError) + + await assert.rejects( + () => SmusUtils.getSsoInstanceInfo(testDomainUrl), + (error: any) => { + assert.strictEqual(error.code, SmusErrorCodes.ApiTimeout) + assert.ok(error.message.includes('timed out after 10 seconds')) + return true + } + ) + }) + + it('should handle login failure errors', async () => { + fetchStub.resolves({ + ok: false, + status: 401, + statusText: 'Unauthorized', + }) + + await assert.rejects( + () => SmusUtils.getSsoInstanceInfo(testDomainUrl), + (error: any) => { + assert.strictEqual(error.code, SmusErrorCodes.SmusLoginFailed) + assert.ok(error.message.includes('401')) + return true + } + ) + }) + + it('should successfully extract SSO instance info', async () => { + const mockResponse = { + ok: true, + json: sinon.stub().resolves({ + redirectUrl: + 'https://example.com/oauth/authorize?client_id=arn%3Aaws%3Asso%3A%3A123456789%3Aapplication%2Fssoins-123%2Fapl-456', + }), + } + fetchStub.resolves(mockResponse) + + const result = await SmusUtils.getSsoInstanceInfo(testDomainUrl) + + assert.strictEqual(result.ssoInstanceId, 'ssoins-123') + assert.strictEqual(result.issuerUrl, 'https://identitycenter.amazonaws.com/ssoins-123') + assert.strictEqual(result.clientId, 'arn:aws:sso::123456789:application/ssoins-123/apl-456') + assert.strictEqual(result.region, testRegion) + }) + + it('should throw error for missing redirect URL', async () => { + const mockResponse = { + ok: true, + json: sinon.stub().resolves({}), + } + fetchStub.resolves(mockResponse) + + await assert.rejects( + () => SmusUtils.getSsoInstanceInfo(testDomainUrl), + (error: any) => { + assert.strictEqual(error.code, 'InvalidLoginResponse') + return true + } + ) + }) + + it('should throw error for missing client_id in redirect URL', async () => { + const mockResponse = { + ok: true, + json: sinon.stub().resolves({ + redirectUrl: 'https://example.com/oauth/authorize', + }), + } + fetchStub.resolves(mockResponse) + + await assert.rejects( + () => SmusUtils.getSsoInstanceInfo(testDomainUrl), + (error: any) => { + assert.strictEqual(error.code, 'InvalidRedirectUrl') + return true + } + ) + }) + + it('should throw error for invalid ARN format', async () => { + const mockResponse = { + ok: true, + json: sinon.stub().resolves({ + redirectUrl: 'https://example.com/oauth/authorize?client_id=invalid-arn', + }), + } + fetchStub.resolves(mockResponse) + + await assert.rejects( + () => SmusUtils.getSsoInstanceInfo(testDomainUrl), + (error: any) => { + assert.strictEqual(error.code, 'InvalidArnFormat') + return true + } + ) + }) + }) + + describe('extractSSOIdFromUserId', () => { + it('should extract SSO ID from valid user ID', () => { + const result = SmusUtils.extractSSOIdFromUserId('user-12345678-abcd-efgh-ijkl-123456789012') + assert.strictEqual(result, '12345678-abcd-efgh-ijkl-123456789012') + }) + + it('should throw error for invalid user ID format', () => { + assert.throws( + () => SmusUtils.extractSSOIdFromUserId('invalid-format'), + /Invalid UserId format: invalid-format/ + ) + }) + + it('should throw error for empty user ID', () => { + assert.throws(() => SmusUtils.extractSSOIdFromUserId(''), /Invalid UserId format: /) + }) + + it('should throw error for user ID without prefix', () => { + assert.throws( + () => SmusUtils.extractSSOIdFromUserId('12345678-abcd-efgh-ijkl-123456789012'), + /Invalid UserId format: 12345678-abcd-efgh-ijkl-123456789012/ + ) + }) + }) + + describe('validateCredentialFields', () => { + it('should not throw for valid credentials', () => { + const validCredentials = { + accessKeyId: 'AKIAIOSFODNN7EXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + sessionToken: + 'AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE', + } + + assert.doesNotThrow(() => { + validateCredentialFields(validCredentials, 'TestError', 'test context') + }) + }) + + it('should throw for missing accessKeyId', () => { + const invalidCredentials = { + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + sessionToken: 'token', + } + + assert.throws( + () => validateCredentialFields(invalidCredentials, 'TestError', 'test context'), + (error: any) => { + assert.ok(error instanceof ToolkitError) + assert.strictEqual(error.code, 'TestError') + assert.ok(error.message.includes('Invalid accessKeyId in test context')) + return true + } + ) + }) + + it('should throw for invalid accessKeyId type', () => { + const invalidCredentials = { + accessKeyId: 123, + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + sessionToken: 'token', + } + + assert.throws( + () => validateCredentialFields(invalidCredentials, 'TestError', 'test context'), + (error: any) => { + assert.ok(error instanceof ToolkitError) + assert.strictEqual(error.code, 'TestError') + assert.ok(error.message.includes('Invalid accessKeyId in test context: number')) + return true + } + ) + }) + + it('should throw for missing secretAccessKey', () => { + const invalidCredentials = { + accessKeyId: 'AKIAIOSFODNN7EXAMPLE', + sessionToken: 'token', + } + + assert.throws( + () => validateCredentialFields(invalidCredentials, 'TestError', 'test context'), + (error: any) => { + assert.ok(error instanceof ToolkitError) + assert.strictEqual(error.code, 'TestError') + assert.ok(error.message.includes('Invalid secretAccessKey in test context')) + return true + } + ) + }) + + it('should throw for missing sessionToken', () => { + const invalidCredentials = { + accessKeyId: 'AKIAIOSFODNN7EXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + } + + assert.throws( + () => validateCredentialFields(invalidCredentials, 'TestError', 'test context'), + (error: any) => { + assert.ok(error instanceof ToolkitError) + assert.strictEqual(error.code, 'TestError') + assert.ok(error.message.includes('Invalid sessionToken in test context')) + return true + } + ) + }) + }) + + describe('isInSmusSpaceEnvironment', () => { + let isSageMakerStub: sinon.SinonStub + let getResourceMetadataStub: sinon.SinonStub + + beforeEach(() => { + isSageMakerStub = sinon.stub(extensionUtilities, 'isSageMaker') + getResourceMetadataStub = sinon.stub(resourceMetadataUtils, 'getResourceMetadata') + }) + + it('should return true when in SMUS space with DataZone domain ID', () => { + isSageMakerStub.withArgs('SMUS').returns(true) + getResourceMetadataStub.returns({ + AdditionalMetadata: { + DataZoneDomainId: 'dz-domain-123', + }, + }) + + const result = SmusUtils.isInSmusSpaceEnvironment() + assert.strictEqual(result, true) + }) + + it('should return false when not in SMUS space', () => { + isSageMakerStub.withArgs('SMUS').returns(false) + isSageMakerStub.withArgs('SMUS-SPACE-REMOTE-ACCESS').returns(false) + + const result = SmusUtils.isInSmusSpaceEnvironment() + assert.strictEqual(result, false) + }) + + it('should return false when in SMUS space but no resource metadata', () => { + isSageMakerStub.withArgs('SMUS').returns(true) + getResourceMetadataStub.returns(undefined) + + const result = SmusUtils.isInSmusSpaceEnvironment() + assert.strictEqual(result, false) + }) + + it('should return false when in SMUS space but no DataZone domain ID', () => { + isSageMakerStub.withArgs('SMUS').returns(true) + getResourceMetadataStub.returns({ + AdditionalMetadata: {}, + }) + + const result = SmusUtils.isInSmusSpaceEnvironment() + assert.strictEqual(result, false) + }) + }) + + describe('isIamUserArn', () => { + it('should return true for IAM user ARN', () => { + const iamUserArn = 'arn:aws:iam::619071339486:user/vabharga-test' + const result = SmusUtils.isIamUserArn(iamUserArn) + assert.strictEqual(result, true) + }) + + it('should return false for IAM role session ARN', () => { + const roleSessionArn = 'arn:aws:sts::123456789012:assumed-role/MyRole/MySession' + const result = SmusUtils.isIamUserArn(roleSessionArn) + assert.strictEqual(result, false) + }) + + it('should return false for IAM role ARN', () => { + const roleArn = 'arn:aws:iam::123456789012:role/MyRole' + const result = SmusUtils.isIamUserArn(roleArn) + assert.strictEqual(result, false) + }) + + it('should return false for undefined ARN', () => { + const result = SmusUtils.isIamUserArn(undefined) + assert.strictEqual(result, false) + }) + + it('should return false for empty string', () => { + const result = SmusUtils.isIamUserArn('') + assert.strictEqual(result, false) + }) + + it('should return false for invalid ARN format', () => { + const result = SmusUtils.isIamUserArn('not-an-arn') + assert.strictEqual(result, false) + }) + + it('should return false for non-IAM ARN', () => { + const s3Arn = 'arn:aws:s3:::my-bucket' + const result = SmusUtils.isIamUserArn(s3Arn) + assert.strictEqual(result, false) + }) + }) + + describe('convertAssumedRoleArnToIamRoleArn', () => { + it('should convert basic assumed role ARN to IAM role ARN', () => { + const stsArn = 'arn:aws:sts::123456789012:assumed-role/MyRole/MySession' + const expected = 'arn:aws:iam::123456789012:role/MyRole' + + const result = SmusUtils.convertAssumedRoleArnToIamRoleArn(stsArn) + assert.strictEqual(result, expected) + }) + + it('should convert assumed role ARN with aws-cn partition', () => { + const stsArn = 'arn:aws-cn:sts::123456789012:assumed-role/MyRole/MySession' + const expected = 'arn:aws-cn:iam::123456789012:role/MyRole' + + const result = SmusUtils.convertAssumedRoleArnToIamRoleArn(stsArn) + assert.strictEqual(result, expected) + }) + + it('should convert assumed role ARN with aws-us-gov partition', () => { + const stsArn = 'arn:aws-us-gov:sts::123456789012:assumed-role/MyRole/MySession' + const expected = 'arn:aws-us-gov:iam::123456789012:role/MyRole' + + const result = SmusUtils.convertAssumedRoleArnToIamRoleArn(stsArn) + assert.strictEqual(result, expected) + }) + + it('should return IAM user ARN as-is', () => { + const iamUserArn = 'arn:aws:iam::619071339486:user/vabharga-test' + const result = SmusUtils.convertAssumedRoleArnToIamRoleArn(iamUserArn) + assert.strictEqual(result, iamUserArn) + }) + + it('should return IAM user ARN with aws-cn partition as-is', () => { + const iamUserArn = 'arn:aws-cn:iam::123456789012:user/my-user' + const result = SmusUtils.convertAssumedRoleArnToIamRoleArn(iamUserArn) + assert.strictEqual(result, iamUserArn) + }) + + it('should return IAM user ARN with aws-us-gov partition as-is', () => { + const iamUserArn = 'arn:aws-us-gov:iam::123456789012:user/my-user' + const result = SmusUtils.convertAssumedRoleArnToIamRoleArn(iamUserArn) + assert.strictEqual(result, iamUserArn) + }) + + it('should return IAM role ARN as-is', () => { + const iamRoleArn = 'arn:aws:iam::123456789012:role/MyRole' + const result = SmusUtils.convertAssumedRoleArnToIamRoleArn(iamRoleArn) + assert.strictEqual(result, iamRoleArn) + }) + + it('should return IAM role ARN with aws-cn partition as-is', () => { + const iamRoleArn = 'arn:aws-cn:iam::123456789012:role/MyRole' + const result = SmusUtils.convertAssumedRoleArnToIamRoleArn(iamRoleArn) + assert.strictEqual(result, iamRoleArn) + }) + + it('should handle IAM user ARN with special characters', () => { + const iamUserArn = 'arn:aws:iam::123456789012:user/path/to/user-name_123' + const result = SmusUtils.convertAssumedRoleArnToIamRoleArn(iamUserArn) + assert.strictEqual(result, iamUserArn) + }) + + it('should throw error for invalid ARN format - missing components', () => { + const invalidArn = 'arn:aws:sts::123456789012:assumed-role/MyRole' + + assert.throws( + () => SmusUtils.convertAssumedRoleArnToIamRoleArn(invalidArn), + (error: Error) => { + assert.ok(error.message.includes('Invalid STS ARN format')) + assert.ok(error.message.includes(invalidArn)) + return true + } + ) + }) + + it('should throw error for empty string', () => { + assert.throws( + () => SmusUtils.convertAssumedRoleArnToIamRoleArn(''), + (error: Error) => { + assert.ok(error.message.includes('Invalid STS ARN format')) + return true + } + ) + }) + }) +}) + +describe('extractAccountIdFromSageMakerArn', () => { + describe('valid ARN formats', () => { + it('should extract account ID from valid ARN', () => { + const arn = 'arn:aws:sagemaker:us-west-2:123456789012:app/domain-id/ce/CodeEditor/default' + const result = extractAccountIdFromSageMakerArn(arn) + + assert.strictEqual(result, '123456789012') + }) + }) + + describe('invalid ARN formats', () => { + it('should throw error for empty ARN', () => { + assert.throws( + () => extractAccountIdFromSageMakerArn(''), + (error: any) => { + assert.ok(error instanceof ToolkitError) + assert.ok(error.message.includes('Invalid SageMaker ARN format')) + return true + } + ) + }) + + it('should throw error for non-ARN string', () => { + assert.throws( + () => extractAccountIdFromSageMakerArn('not-an-arn'), + (error: any) => { + assert.ok(error instanceof ToolkitError) + assert.ok(error.message.includes('Invalid SageMaker ARN format')) + return true + } + ) + }) + + it('should throw error for wrong service', () => { + const arn = 'arn:aws:s3:us-east-1:123456789012:bucket/my-bucket' + assert.throws( + () => extractAccountIdFromSageMakerArn(arn), + (error: any) => { + assert.ok(error instanceof ToolkitError) + assert.ok(error.message.includes('Invalid SageMaker ARN format')) + return true + } + ) + }) + + it('should throw error for missing account ID', () => { + const arn = 'arn:aws:sagemaker:us-east-1::space/domain/space' + assert.throws( + () => extractAccountIdFromSageMakerArn(arn), + (error: any) => { + assert.ok(error instanceof ToolkitError) + assert.ok(error.message.includes('Invalid SageMaker ARN format')) + return true + } + ) + }) + }) +}) + +describe('extractAccountIdFromResourceMetadata', () => { + let getResourceMetadataStub: sinon.SinonStub + + beforeEach(() => { + getResourceMetadataStub = sinon.stub(resourceMetadataUtils, 'getResourceMetadata') + }) + + afterEach(() => { + sinon.restore() + }) + + it('should extract account ID from ResourceArn successfully', async () => { + const testAccountId = '123456789012' + const testResourceArn = `arn:aws:sagemaker:us-east-1:${testAccountId}:app/domain-id/appName/CodeEditor/default` + + getResourceMetadataStub.returns({ + ResourceArn: testResourceArn, + }) + + const result = await extractAccountIdFromResourceMetadata() + + assert.strictEqual(result, testAccountId) + assert.ok(getResourceMetadataStub.called) + }) + + it('should throw error when ResourceArn is missing', async () => { + getResourceMetadataStub.returns({}) + + await assert.rejects( + () => extractAccountIdFromResourceMetadata(), + (err: Error) => { + return err.message.includes( + 'Failed to extract AWS account ID from ResourceArn in SMUS space environment' + ) + } + ) + }) + + it('should throw error when extractAccountIdFromSageMakerArn fails', async () => { + const testResourceArn = 'invalid-arn' + getResourceMetadataStub.returns({ + ResourceArn: testResourceArn, + }) + + await assert.rejects( + () => extractAccountIdFromResourceMetadata(), + (err: Error) => { + return err.message.includes( + 'Failed to extract AWS account ID from ResourceArn in SMUS space environment' + ) + } + ) + }) +}) + +describe('isCredentialExpirationError', () => { + describe('should return true for credential expiration errors', () => { + it('should detect ExpiredTokenException by error name (exact match)', () => { + const error = { + name: 'ExpiredTokenException', + message: 'Token has expired', + } + + const result = isCredentialExpirationError(error) + assert.strictEqual(result, true) + }) + + it('should detect ExpiredTokenException in error message', () => { + const error = { + name: 'SomeOtherError', + message: 'Request failed with ExpiredTokenException: Token has expired', + } + + const result = isCredentialExpirationError(error) + assert.strictEqual(result, true) + }) + }) + + describe('should return false for non-expiration errors', () => { + it('should return false for different error names', () => { + const error = { + name: 'AccessDeniedException', + message: 'Access denied', + } + + const result = isCredentialExpirationError(error) + assert.strictEqual(result, false) + }) + }) +}) + +describe('isIamDomain', () => { + it('should return false for V1 domains regardless of IamSignIns', () => { + const result = isIamDomain({ + domainVersion: DomainVersionV1, + iamSignIns: [IamSignInRole, IamSignInUser], + }) + assert.strictEqual(result, false) + }) + + it('should return true for V2 domain with both IAM_ROLE and IAM_USER', () => { + const result = isIamDomain({ + domainVersion: DomainVersionV2, + iamSignIns: [IamSignInRole, IamSignInUser], + }) + assert.strictEqual(result, true) + }) + + it('should return false for V2 domain with only IAM_ROLE', () => { + const result = isIamDomain({ + domainVersion: DomainVersionV2, + iamSignIns: [IamSignInRole], + }) + assert.strictEqual(result, false) + }) + + it('should return false for V2 domain with only IAM_USER', () => { + const result = isIamDomain({ + domainVersion: DomainVersionV2, + iamSignIns: [IamSignInUser], + }) + assert.strictEqual(result, false) + }) + + it('should return false for V2 domain with missing IamSignIns', () => { + const result = isIamDomain({ + domainVersion: DomainVersionV2, + iamSignIns: undefined, + }) + assert.strictEqual(result, false) + }) + + it('should return false for undefined domain version', () => { + const result = isIamDomain({ + domainVersion: undefined, + iamSignIns: [IamSignInRole, IamSignInUser], + }) + assert.strictEqual(result, false) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/shared/utils/resourceMetadataUtils.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/utils/resourceMetadataUtils.test.ts new file mode 100644 index 00000000000..3580a730fbc --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/utils/resourceMetadataUtils.test.ts @@ -0,0 +1,292 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as sinon from 'sinon' +import { fs } from '../../../../shared/fs/fs' +import * as extensionUtilities from '../../../../shared/extensionUtilities' +import { + initializeResourceMetadata, + getResourceMetadata, + resourceMetadataFileExists, + resetResourceMetadata, + ResourceMetadata, +} from '../../../../sagemakerunifiedstudio/shared/utils/resourceMetadataUtils' + +describe('resourceMetadataUtils', function () { + let sandbox: sinon.SinonSandbox + + const mockMetadata: ResourceMetadata = { + AppType: 'JupyterServer', + DomainId: 'domain-12345', + SpaceName: 'test-space', + UserProfileName: 'test-user', + ExecutionRoleArn: 'arn:aws:iam::123456789012:role/test-role', + ResourceArn: 'arn:aws:sagemaker:us-west-2:123456789012:app/domain-12345/test-user/jupyterserver/test-app', + ResourceName: 'test-app', + AppImageVersion: '1.0.0', + AdditionalMetadata: { + DataZoneDomainId: 'dz-domain-123', + DataZoneDomainRegion: 'us-west-2', + DataZoneEndpoint: 'https://datazone.us-west-2.amazonaws.com', + DataZoneEnvironmentId: 'env-123', + DataZoneProjectId: 'project-456', + DataZoneScopeName: 'test-scope', + DataZoneStage: 'prod', + DataZoneUserId: 'user-789', + PrivateSubnets: 'subnet-123,subnet-456', + ProjectS3Path: 's3://test-bucket/project/', + SecurityGroup: 'sg-123456789', + }, + ResourceArnCaseSensitive: + 'arn:aws:sagemaker:us-west-2:123456789012:app/domain-12345/test-user/JupyterServer/test-app', + IpAddressType: 'IPv4', + } + + beforeEach(function () { + sandbox = sinon.createSandbox() + resetResourceMetadata() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('initializeResourceMetadata()', function () { + it('should initialize metadata when file exists and is valid JSON', async function () { + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(mockMetadata)) + + await initializeResourceMetadata() + const result = getResourceMetadata() + + assert.deepStrictEqual(result, mockMetadata) + }) + + it('should not initialize when not in SMUS environment', async function () { + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(false) + + await initializeResourceMetadata() + const result = getResourceMetadata() + + assert.strictEqual(result, undefined) + }) + + it('should not throw when file does not exist', async function () { + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(false) + + await initializeResourceMetadata() + const result = getResourceMetadata() + + assert.strictEqual(result, undefined) + }) + + it('should handle invalid JSON gracefully', async function () { + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves('{ invalid json }') + + await initializeResourceMetadata() + const result = getResourceMetadata() + + assert.strictEqual(result, undefined) + }) + + it('should handle file read errors gracefully', async function () { + const error = new Error('File read error') + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').rejects(error) + + await initializeResourceMetadata() + const result = getResourceMetadata() + + assert.strictEqual(result, undefined) + }) + + it('should handle metadata with missing optional fields', async function () { + const minimalMetadata: ResourceMetadata = { + DomainId: 'domain-123', + } + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(minimalMetadata)) + + await initializeResourceMetadata() + const result = getResourceMetadata() + + assert.deepStrictEqual(result, minimalMetadata) + }) + + it('should handle metadata with empty AdditionalMetadata', async function () { + const metadataWithEmptyAdditional: ResourceMetadata = { + DomainId: 'domain-123', + AdditionalMetadata: {}, + } + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(metadataWithEmptyAdditional)) + + await initializeResourceMetadata() + const result = getResourceMetadata() + + assert.deepStrictEqual(result, metadataWithEmptyAdditional) + }) + + it('should handle empty JSON file', async function () { + const emptyMetadata: ResourceMetadata = {} + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(emptyMetadata)) + + await initializeResourceMetadata() + const result = getResourceMetadata() + + assert.deepStrictEqual(result, emptyMetadata) + }) + + it('should handle very large JSON files', async function () { + const largeMetadata = { + ...mockMetadata, + LargeField: 'x'.repeat(10000), + } + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(largeMetadata)) + + await initializeResourceMetadata() + const result = getResourceMetadata() + + assert.strictEqual((result as any).LargeField?.length, 10000) + }) + + it('should handle JSON with unexpected additional fields', async function () { + const metadataWithExtraFields = { + ...mockMetadata, + UnexpectedField: 'unexpected-value', + AdditionalMetadata: { + ...mockMetadata.AdditionalMetadata, + UnexpectedNestedField: 'unexpected-nested-value', + }, + } + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(metadataWithExtraFields)) + + await initializeResourceMetadata() + const result = getResourceMetadata() + + assert.strictEqual((result as any).UnexpectedField, 'unexpected-value') + assert.strictEqual((result as any).AdditionalMetadata?.UnexpectedNestedField, 'unexpected-nested-value') + }) + + it('should handle JSON with undefined values', async function () { + const metadataWithUndefined = { + DomainId: undefined, + AdditionalMetadata: { + DataZoneDomainId: undefined, + }, + } + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(metadataWithUndefined)) + + await initializeResourceMetadata() + const result = getResourceMetadata() + + assert.strictEqual(result?.DomainId, undefined) + assert.strictEqual(result?.AdditionalMetadata?.DataZoneDomainId, undefined) + }) + }) + + describe('getResourceMetadata()', function () { + it('should return undefined when not initialized', function () { + const result = getResourceMetadata() + assert.strictEqual(result, undefined) + }) + + it('should return cached metadata after initialization', async function () { + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(mockMetadata)) + + await initializeResourceMetadata() + + const result = getResourceMetadata() + assert.deepStrictEqual(result, mockMetadata) + }) + + it('should return the same instance on multiple calls', async function () { + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(mockMetadata)) + + await initializeResourceMetadata() + + const result1 = getResourceMetadata() + const result2 = getResourceMetadata() + + assert.strictEqual(result1, result2) + assert.deepStrictEqual(result1, mockMetadata) + }) + }) + + describe('resetResourceMetadata()', function () { + it('should reset cached metadata and allow re-initialization', async function () { + sandbox.stub(extensionUtilities, 'isSageMaker').withArgs('SMUS').returns(true) + const existsFileStub = sandbox.stub(fs, 'existsFile').resolves(true) + const readFileTextStub = sandbox.stub(fs, 'readFileText').resolves(JSON.stringify(mockMetadata)) + + await initializeResourceMetadata() + const cached1 = getResourceMetadata() + assert.deepStrictEqual(cached1, mockMetadata) + + sinon.assert.calledOnce(existsFileStub) + sinon.assert.calledOnce(readFileTextStub) + + resetResourceMetadata() + + const cached2 = getResourceMetadata() + assert.strictEqual(cached2, undefined) + + await initializeResourceMetadata() + const cached3 = getResourceMetadata() + assert.deepStrictEqual(cached3, mockMetadata) + + sinon.assert.calledTwice(existsFileStub) + sinon.assert.calledTwice(readFileTextStub) + }) + }) + + describe('resourceMetadataFileExists()', function () { + it('should return true when file exists', async function () { + const existsStub = sandbox.stub(fs, 'existsFile').resolves(true) + + const result = await resourceMetadataFileExists() + + assert.strictEqual(result, true) + sinon.assert.calledOnceWithExactly(existsStub, '/opt/ml/metadata/resource-metadata.json') + }) + + it('should return false when file does not exist', async function () { + sandbox.stub(fs, 'existsFile').resolves(false) + + const result = await resourceMetadataFileExists() + + assert.strictEqual(result, false) + }) + + it('should return false and log error when fs.existsFile throws', async function () { + const error = new Error('Permission denied') + sandbox.stub(fs, 'existsFile').rejects(error) + + const result = await resourceMetadataFileExists() + + assert.strictEqual(result, false) + }) + }) +}) diff --git a/packages/core/src/test/sagemakerunifiedstudio/testUtils.ts b/packages/core/src/test/sagemakerunifiedstudio/testUtils.ts new file mode 100644 index 00000000000..ce1a706325d --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/testUtils.ts @@ -0,0 +1,89 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sinon from 'sinon' + +/** + * Creates a mock extension context for SageMaker Unified Studio tests + */ +export function createMockExtensionContext(): any { + return { + subscriptions: [], + workspaceState: { + get: sinon.stub(), + update: sinon.stub(), + }, + globalState: { + get: sinon.stub(), + update: sinon.stub(), + }, + } +} + +/** + * Creates a mock S3 connection for SageMaker Unified Studio tests + */ +export function createMockS3Connection() { + return { + connectionId: 'conn-123', + name: 'project.s3_default_folder', + type: 'S3Connection', + props: { + s3Properties: { + s3Uri: 's3://test-bucket/domain/project/', + }, + }, + } +} + +/** + * Creates a mock credentials provider for SageMaker Unified Studio tests + */ +export function createMockCredentialsProvider() { + return { + getCredentials: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + }), + getDomainAccountId: async () => '123456789012', + } +} +/** + * Creates a mock unauthenticated auth provider for SageMaker Unified Studio tests + */ +export function createMockUnauthenticatedAuthProvider(): any { + return { + isConnected: sinon.stub().returns(false), + isConnectionValid: sinon.stub().returns(false), + activeConnection: undefined, + onDidChange: sinon.stub().returns({ dispose: sinon.stub() }), + } +} /** + * + Creates a mock space node for SageMaker Unified Studio tests + */ +export function createMockSpaceNode(): any { + return { + resource: { + sageMakerClient: {}, + DomainSpaceKey: 'test-space-key', + regionCode: 'us-east-1', + getParent: sinon.stub().returns({ + getAuthProvider: sinon.stub().returns({ + activeConnection: { domainId: 'test-domain' }, + getDomainAccountId: sinon.stub().resolves('123456789012'), + }), + getProjectId: sinon.stub().returns('test-project'), + }), + }, + getParent: sinon.stub().returns({ + getAuthProvider: sinon.stub().returns({ + activeConnection: { domainId: 'test-domain' }, + getDomainAccountId: sinon.stub().resolves('123456789012'), + }), + getProjectId: sinon.stub().returns('test-project'), + }), + } +} diff --git a/packages/core/src/test/sagemakerunifiedstudio/uriHandlers.test.ts b/packages/core/src/test/sagemakerunifiedstudio/uriHandlers.test.ts new file mode 100644 index 00000000000..ba3aff2b629 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/uriHandlers.test.ts @@ -0,0 +1,88 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { SearchParams } from '../../shared/vscode/uriHandler' +import { parseConnectParams } from '../../sagemakerunifiedstudio/uriHandlers' + +describe('SMUS URI Handler', function () { + describe('parseConnectParams', function () { + const validParams = { + connection_identifier: 'arn:aws:sagemaker:us-west-2:123456789012:space/d-abc123/my-space', + domain: 'd-abc123', + user_profile: 'test-user', + session: 'sess-abc123', + ws_url: 'wss://ssm.us-west-2.amazonaws.com/stream', + 'cell-number': '1', + token: 'bearer-token-xyz', + } + + it('successfully parses all required parameters', function () { + const query = new SearchParams(validParams) + const result = parseConnectParams(query) + + assert.strictEqual(result.connection_identifier, validParams.connection_identifier) + assert.strictEqual(result.domain, validParams.domain) + assert.strictEqual(result.user_profile, validParams.user_profile) + assert.strictEqual(result.session, validParams.session) + assert.strictEqual(result.ws_url, validParams.ws_url) + assert.strictEqual(result['cell-number'], validParams['cell-number']) + assert.strictEqual(result.token, validParams.token) + }) + + it('throws error when required parameters are missing', function () { + const requiredParams = [ + 'connection_identifier', + 'domain', + 'user_profile', + 'session', + 'ws_url', + 'cell-number', + 'token', + ] as const + + for (const param of requiredParams) { + const { [param]: _removed, ...paramsWithoutOne } = validParams + const query = new SearchParams(paramsWithoutOne) + + assert.throws( + () => parseConnectParams(query), + new RegExp(`${param}.*must be provided`), + `Should throw error for missing ${param}` + ) + } + }) + + it('handles optional parameters correctly', function () { + // Test with all optional parameters present + const paramsWithAllOptional = { + ...validParams, + app_type: 'CodeEditor', + smus_domain_id: 'smus-domain-789', + smus_domain_account_id: '111222333444', + smus_project_id: 'project-999', + smus_domain_region: 'eu-west-1', + } + const queryWithOptional = new SearchParams(paramsWithAllOptional) + const resultWithOptional = parseConnectParams(queryWithOptional) + + assert.strictEqual(resultWithOptional.app_type, 'CodeEditor') + assert.strictEqual(resultWithOptional.smus_domain_id, 'smus-domain-789') + assert.strictEqual(resultWithOptional.smus_domain_account_id, '111222333444') + assert.strictEqual(resultWithOptional.smus_project_id, 'project-999') + assert.strictEqual(resultWithOptional.smus_domain_region, 'eu-west-1') + + // Test without optional parameters - should return undefined + const queryWithoutOptional = new SearchParams(validParams) + const resultWithoutOptional = parseConnectParams(queryWithoutOptional) + + assert.strictEqual(resultWithoutOptional.app_type, undefined) + assert.strictEqual(resultWithoutOptional.smus_domain_id, undefined) + assert.strictEqual(resultWithoutOptional.smus_domain_account_id, undefined) + assert.strictEqual(resultWithoutOptional.smus_project_id, undefined) + assert.strictEqual(resultWithoutOptional.smus_domain_region, undefined) + }) + }) +}) diff --git a/packages/core/src/test/setupUtil.ts b/packages/core/src/test/setupUtil.ts index d0a4cd0b594..5e00b06ff5e 100644 --- a/packages/core/src/test/setupUtil.ts +++ b/packages/core/src/test/setupUtil.ts @@ -4,7 +4,8 @@ */ import { parse } from '@aws-sdk/util-arn-parser' -import { Lambda, STS } from 'aws-sdk' +import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda' +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts' import * as vscode from 'vscode' import { getLogger } from '../shared/logger' import { hasKey } from '../shared/utilities/tsUtils' @@ -135,13 +136,13 @@ export function patchObjectDescriptor, U extends k async function createLambdaClient(functionId: string) { if (!functionId.startsWith('arn:aws:lambda')) { - return Object.assign(new Lambda(), { isCrossAccount: false }) + return Object.assign(new LambdaClient({}), { isCrossAccount: false }) } - const sts = new STS() + const sts = new STSClient({}) const { region, accountId } = parse(functionId) - const identity = await sts.getCallerIdentity().promise() - const client = new Lambda({ region }) + const identity = await sts.send(new GetCallerIdentityCommand({})) + const client = new LambdaClient({ region }) return Object.assign(client, { isCrossAccount: identity.Account !== accountId }) } @@ -149,14 +150,15 @@ async function createLambdaClient(functionId: string) { export async function invokeLambda(id: string, request: unknown): Promise { const client = await createLambdaClient(id) const response = await client - .invoke({ - FunctionName: id, - // Setting this to `Tail` with cross account calls results in - // `AccessDeniedException: Cross-account log access is not allowed` - LogType: client.isCrossAccount ? 'None' : 'Tail', - Payload: JSON.stringify(request), - }) - .promise() + .send( + new InvokeCommand({ + FunctionName: id, + // Setting this to `Tail` with cross account calls results in + // `AccessDeniedException: Cross-account log access is not allowed` + LogType: client.isCrossAccount ? 'None' : 'Tail', + Payload: JSON.stringify(request), + }) + ) .catch((err) => { if (err instanceof Error) { err.message = maskArns(err.message) @@ -168,10 +170,10 @@ export async function invokeLambda(id: string, request: unknown): Promise { const expectedStackName = 'myStack' @@ -63,6 +66,11 @@ describe('DeployedResourceNode', () => { resourceArn: 'arn:aws:apigateway:us-east-1::/apis/my-apgw', contextValue: 'awsApiGatewayNode', }, + { + explorerNode: sinon.stub(LambdaCapacityProviderNode), + resourceArn: 'arn:aws:lambda:us-east-1:123456789012:capacity-provider:my-capacity-provider-name', + contextValue: 'awsCapacityProviderNode', + }, ].map(({ explorerNode, resourceArn, contextValue }) => getDeployedResource(explorerNode, resourceArn, contextValue)) describe('constructor', () => { @@ -137,6 +145,7 @@ describe('generateDeployedNode', () => { label: 'iam', getCredentials: sinon.stub(), state: 'valid', + endpointUrl: undefined, } const lambdaDeployedNodeInput = { @@ -170,7 +179,7 @@ describe('generateDeployedNode', () => { FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:my-project-lambda-function', Runtime: 'python3.12', }, - } as AWS.Lambda.GetFunctionResponse + } as GetFunctionResponse mockDefaultLambdaClientInstance.getFunction.resolves(defaultLambdaClientGetFunctionResponse) @@ -178,12 +187,12 @@ describe('generateDeployedNode', () => { const expectedFunctionName = 'my-project-lambda-function' const expectedFunctionExplorerNodeTooltip = `${expectedFunctionName}${os.EOL}${expectedFunctionArn}` - const deployedResourceNodes = await generateDeployedNode( + const deployedResourceNodes = (await generateDeployedNode( lambdaDeployedNodeInput.deployedResource, lambdaDeployedNodeInput.regionCode, lambdaDeployedNodeInput.stackName, lambdaDeployedNodeInput.resourceTreeEntity - ) + )) as DeployedResourceNode[] const deployedResourceNodeExplorerNode: LambdaFunctionNode = validateBasicProperties( deployedResourceNodes, @@ -258,7 +267,7 @@ describe('generateDeployedNode', () => { const expectedS3BucketName = 'my-project-source-bucket-physical-id' const deployedResourceNodeExplorerNode: S3BucketNode = validateBasicProperties( - deployedResourceNodes, + deployedResourceNodes as DeployedResourceNode[], expectedS3BucketArn, 'awsS3BucketNode', expectedRegionCode, @@ -333,7 +342,7 @@ describe('generateDeployedNode', () => { ) const deployedResourceNodeExplorerNode: RestApiNode = validateBasicProperties( - deployedResourceNodes, + deployedResourceNodes as DeployedResourceNode[], expectedApiGatewayArn, 'awsApiGatewayNode', expectedRegionCode, @@ -350,6 +359,50 @@ describe('generateDeployedNode', () => { }) }) + describe('LambdaCapacityProviderNode', () => { + const capacityProviderDeployedNodeInput = { + deployedResource: { + LogicalResourceId: 'MyCapacityProvider', + PhysicalResourceId: 'my-project-lambda-physical-id', + }, + regionCode: expectedRegionCode, + stackName: expectedStackName, + resourceTreeEntity: { + Id: 'MyCapacityProvider', + Type: 'AWS::Serverless::CapacityProvider', + }, + } + beforeEach(() => { + // Default mock account ID for testing + sandbox.stub(DefaultAwsContext.prototype, 'getCredentialAccountId').returns('123456789012') + }) + it('should return a DeployedResourceNode for valid Lambda Capacity Provider happy path', async () => { + const deployedResourceNodes = await generateDeployedNode( + capacityProviderDeployedNodeInput.deployedResource, + capacityProviderDeployedNodeInput.regionCode, + capacityProviderDeployedNodeInput.stackName, + capacityProviderDeployedNodeInput.resourceTreeEntity + ) + + const expectedCapacityProviderArn = + 'arn:aws:lambda:us-west-2:123456789012:capacity-provider:my-project-lambda-physical-id' + const expectedCapacityProviderName = 'MyCapacityProvider' + + const deployedResourceNodeExplorerNode: LambdaCapacityProviderNode = validateBasicProperties( + deployedResourceNodes as DeployedResourceNode[], + expectedCapacityProviderArn, + 'awsCapacityProviderNode', + expectedRegionCode, + expectedStackName, + LambdaCapacityProviderNode + ) + assert.strictEqual(deployedResourceNodeExplorerNode.contextValue, 'awsCapacityProviderNode') + assert.strictEqual(deployedResourceNodeExplorerNode.name, expectedCapacityProviderName) + assert.strictEqual(deployedResourceNodeExplorerNode.regionCode, expectedRegionCode) + assert.strictEqual(deployedResourceNodeExplorerNode.iconPath, getIcon('vscode-gear')) + }) + }) + describe('UnsupportedResourceNode', () => { const unsupportTypeInput = { deployedResource: { diff --git a/packages/core/src/test/shared/applicationBuilder/explorer/nodes/resourceNode.test.ts b/packages/core/src/test/shared/applicationBuilder/explorer/nodes/resourceNode.test.ts index 42486ea267b..79f46451988 100644 --- a/packages/core/src/test/shared/applicationBuilder/explorer/nodes/resourceNode.test.ts +++ b/packages/core/src/test/shared/applicationBuilder/explorer/nodes/resourceNode.test.ts @@ -8,10 +8,15 @@ import { ecrRepositoryType, s3BucketType, SERVERLESS_FUNCTION_TYPE, + SERVERLESS_CAPACITY_PROVIDER_TYPE, } from '../../../../../shared/cloudformation/cloudformation' import assert from 'assert' import { ResourceTreeEntity, SamAppLocation } from '../../../../../awsService/appBuilder/explorer/samProject' -import { generateResourceNodes, ResourceNode } from '../../../../../awsService/appBuilder/explorer/nodes/resourceNode' +import { + generateResourceNodes, + ResourceNode, + generateLambdaNodeFromResource, +} from '../../../../../awsService/appBuilder/explorer/nodes/resourceNode' import { getIcon } from '../../../../../shared/icons' import * as DeployedResourceNodeModule from '../../../../../awsService/appBuilder/explorer/nodes/deployedNode' import * as sinon from 'sinon' @@ -19,6 +24,115 @@ import { afterEach } from 'mocha' import { DeployedResourceNode } from '../../../../../awsService/appBuilder/explorer/nodes/deployedNode' import { PropertyNode } from '../../../../../awsService/appBuilder/explorer/nodes/propertyNode' import { StackResource } from '../../../../../lambda/commands/listSamResources' +import { LambdaFunctionNode } from '../../../../../lambda/explorer/lambdaFunctionNode' +import { ToolkitError } from '../../../../../shared/errors' + +describe('generateLambdaNodeFromResource', () => { + let generateDeployedNodeStub: sinon.SinonStub + const resourceMock = { + deployedResource: { + LogicalResourceId: 'TestFunction', + PhysicalResourceId: 'arn:aws:lambda:us-west-2:123456789012:function:TestFunction', + }, + region: 'us-west-2', + stackName: 'TestStack', + resource: { Id: 'TestFunction', Type: SERVERLESS_FUNCTION_TYPE }, + projectRoot: vscode.Uri.parse('myworkspace/myprojectrootfolder'), + location: vscode.Uri.parse('myworkspace/myprojectrootfolder/template.yaml'), + workspaceFolder: { + uri: vscode.Uri.parse('myworkspace'), + name: 'my-workspace', + index: 0, + }, + functionArn: 'arn:aws:lambda:us-west-2:123456789012:function:TestFunction', + } + + beforeEach(() => { + generateDeployedNodeStub = sinon.stub(DeployedResourceNodeModule, 'generateDeployedNode') + }) + + afterEach(() => { + sinon.restore() + }) + + it('should successfully generate LambdaFunctionNode from resource', async () => { + const mockLambdaNode = {} as LambdaFunctionNode + const mockDeployedNode = { + resource: { + explorerNode: mockLambdaNode, + }, + } as DeployedResourceNode + + generateDeployedNodeStub.resolves([mockDeployedNode]) + const resource = resourceMock + const result = await generateLambdaNodeFromResource(resource) + + assert.strictEqual(result, mockLambdaNode) + assert( + generateDeployedNodeStub.calledOnceWith( + resource.deployedResource, + resource.region, + resource.stackName, + resource.resource, + resource.projectRoot + ) + ) + }) + + it('should throw error when deployedResource is missing', async () => { + const resource = { + region: 'us-west-2', + stackName: 'TestStack', + resource: { Id: 'TestFunction', Type: SERVERLESS_FUNCTION_TYPE }, + } + + await assert.rejects( + async () => await generateLambdaNodeFromResource(resource as any), + ToolkitError, + 'Error getting Lambda info from Appbuilder Node, please check your connection' + ) + }) + + it('should throw error when region is missing', async () => { + const resource = { + deployedResource: { LogicalResourceId: 'TestFunction' }, + stackName: 'TestStack', + resource: { Id: 'TestFunction', Type: SERVERLESS_FUNCTION_TYPE }, + } + + await assert.rejects( + async () => await generateLambdaNodeFromResource(resource as any), + ToolkitError, + 'Error getting Lambda info from Appbuilder Node, please check your connection' + ) + }) + + it('should throw error when generateDeployedNode returns no nodes', async () => { + generateDeployedNodeStub.resolves([]) + + const resource = resourceMock + + await assert.rejects( + async () => await generateLambdaNodeFromResource(resource), + ToolkitError, + 'Error getting Lambda info from Appbuilder Node, please check your connection' + ) + }) + + it('should throw error when generateDeployedNode returns multiple nodes', async () => { + const mockDeployedNode1 = {} as DeployedResourceNode + const mockDeployedNode2 = {} as DeployedResourceNode + generateDeployedNodeStub.resolves([mockDeployedNode1, mockDeployedNode2]) + + const resource = resourceMock + + await assert.rejects( + async () => await generateLambdaNodeFromResource(resource), + ToolkitError, + 'Error getting Lambda info from Appbuilder Node, please check your connection' + ) + }) +}) describe('ResourceNode', () => { const lambdaResourceTreeEntity = { @@ -34,7 +148,7 @@ describe('ResourceNode', () => { Method: undefined, }, ], - } + } satisfies ResourceTreeEntity const workspaceFolder = { uri: vscode.Uri.parse('myworkspace'), name: 'my-workspace', @@ -161,6 +275,7 @@ describe('ResourceNode', () => { { type: appRunnerType, expectedIconKey: 'aws-apprunner-service' }, { type: ecrRepositoryType, expectedIconKey: 'aws-ecr-registry' }, { type: 'Unsupported', expectedIconKey: 'info' }, + { type: SERVERLESS_CAPACITY_PROVIDER_TYPE, expectedIconKey: 'gear' }, ] testCase.map((test) => { @@ -175,7 +290,7 @@ describe('ResourceNode', () => { }) describe('getTreeItem', () => { - it('should generate correct TreeItem without none collapsible state given no deployed resource', () => { + it('should generate correct TreeItem with collapsible state given no deployed resource', () => { const resourceNode = new ResourceNode(samAppLocation, lambdaResourceTreeEntity) const treeItem = resourceNode.getTreeItem() @@ -184,10 +299,10 @@ describe('ResourceNode', () => { assert.strictEqual(treeItem.resourceUri, samAppLocation.samTemplateUri) assert.strictEqual(treeItem.contextValue, 'awsAppBuilderResourceNode.function') assert.strictEqual(treeItem.iconPath, getIcon('aws-lambda-function')) - assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) }) - it('should generate correct TreeItem without collapsed state given node with deployed resource', () => { + it('should generate correct TreeItem without collapsed state given node with deployed resource', () => { const resourceNode = new ResourceNode( samAppLocation, lambdaResourceTreeEntity, @@ -201,8 +316,8 @@ describe('ResourceNode', () => { assert.strictEqual(treeItem.label, 'MyFunction') assert.strictEqual(treeItem.tooltip, samAppLocation.samTemplateUri.toString()) assert.strictEqual(treeItem.resourceUri, samAppLocation.samTemplateUri) - assert.strictEqual(treeItem.contextValue, 'awsAppBuilderResourceNode.function') - assert.strictEqual(treeItem.iconPath, getIcon('aws-lambda-function')) + assert.strictEqual(treeItem.contextValue, 'awsAppBuilderResourceNode.deployed-function') + assert.strictEqual(treeItem.iconPath, getIcon('aws-lambda-deployed-function')) assert.strictEqual(treeItem.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) }) }) diff --git a/packages/core/src/test/shared/awsClientBuilderV3.test.ts b/packages/core/src/test/shared/awsClientBuilderV3.test.ts index 47fc7430e98..2844e2756c1 100644 --- a/packages/core/src/test/shared/awsClientBuilderV3.test.ts +++ b/packages/core/src/test/shared/awsClientBuilderV3.test.ts @@ -4,7 +4,6 @@ */ import sinon from 'sinon' import assert from 'assert' -import { version } from 'vscode' import { getClientId } from '../../shared/telemetry/util' import { FakeMemento } from '../fakeExtensionContext' import { FakeAwsContext } from '../utilities/fakeAwsContext' @@ -45,12 +44,34 @@ describe('AwsClientBuilderV3', function () { const service = builder.createAwsService({ serviceClient: Client }) const clientId = getClientId(new GlobalState(new FakeMemento())) - assert.ok(service.config.userAgent) - assert.strictEqual( - service.config.userAgent![0][0].replace('---Insiders', ''), - `AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/${version} ClientId/${clientId}` + // The AWS SDK accepts customUserAgent as input and exposes it in config + const userAgentConfig = service.config.customUserAgent + assert.ok(userAgentConfig, 'customUserAgent should exist in config') + + const pairs = userAgentConfig as [string, string][] + assert.ok(Array.isArray(pairs), 'customUserAgent should be an array') + assert.ok(pairs.length >= 3, `Expected at least 3 pairs, got ${pairs.length}`) + + // Check for toolkit pair (could be AWS-Toolkit-For-VSCode or AmazonQ-For-VSCode) + const toolkitPair = pairs.find( + (p) => + Array.isArray(p) && + typeof p[0] === 'string' && + (p[0].includes('AWS-Toolkit-For-VSCode') || p[0].includes('AmazonQ-For-VSCode')) ) - assert.strictEqual(service.config.userAgent![0][1], extensionVersion) + assert.ok(toolkitPair, 'Expected to find toolkit pair') + assert.strictEqual(toolkitPair[1], extensionVersion) + + // Check for platform pair + const platformPair = pairs.find( + (p) => Array.isArray(p) && typeof p[0] === 'string' && p[0].includes('Visual-Studio-Code') + ) + assert.ok(platformPair, 'Expected to find platform pair') + + // Check for ClientId pair + const clientIdPair = pairs.find((p) => Array.isArray(p) && p[0] === 'ClientId') + assert.ok(clientIdPair, 'Expected to find ClientId pair') + assert.strictEqual(clientIdPair[1], clientId) }) it('adds region to client', function () { @@ -60,22 +81,76 @@ describe('AwsClientBuilderV3', function () { assert.strictEqual(service.config.region, 'us-west-2') }) + it('adds endpoint URL from context to client', function () { + const testEndpointUrl = 'https://custom-endpoint.example.com' + const fakeContext = new FakeAwsContext({ + contextCredentials: { + credentials: {} as any, + credentialsId: 'test', + accountId: '123456789012', + endpointUrl: testEndpointUrl, + }, + }) + const builderWithEndpoint = new AWSClientBuilderV3(fakeContext) + + const service = builderWithEndpoint.createAwsService({ serviceClient: Client }) + + assert.strictEqual(service.config.endpoint, testEndpointUrl) + }) + + it('does not set endpoint when context has no endpoint URL', function () { + const fakeContext = new FakeAwsContext({ + contextCredentials: { + credentials: {} as any, + credentialsId: 'test', + accountId: '123456789012', + }, + }) + const builderWithoutEndpoint = new AWSClientBuilderV3(fakeContext) + + const service = builderWithoutEndpoint.createAwsService({ serviceClient: Client }) + + assert.strictEqual(service.config.endpoint, undefined) + }) + + it('does not set endpoint when context has undefined endpoint URL', function () { + const fakeContext = new FakeAwsContext({ + contextCredentials: { + credentials: {} as any, + credentialsId: 'test', + accountId: '123456789012', + endpointUrl: undefined, + }, + }) + const builderWithUndefinedEndpoint = new AWSClientBuilderV3(fakeContext) + + const service = builderWithUndefinedEndpoint.createAwsService({ serviceClient: Client }) + + assert.strictEqual(service.config.endpoint, undefined) + }) + it('adds Client-Id to user agent', function () { const service = builder.createAwsService({ serviceClient: Client }) const clientId = getClientId(new GlobalState(new FakeMemento())) - const regex = new RegExp(`ClientId/${clientId}`) - assert.ok(service.config.userAgent![0][0].match(regex)) + const pairs = service.config.customUserAgent as [string, string][] + const clientIdPair = pairs.find((p) => p[0] === 'ClientId') + assert.ok(clientIdPair, 'Should include ClientId pair') + assert.strictEqual(clientIdPair[1], clientId) }) it('does not override custom user-agent if specified in options', function () { + const customUserAgent: [string, string][] = [['CUSTOM-USER-AGENT', '1.0.0']] const service = builder.createAwsService({ serviceClient: Client, clientOptions: { - userAgent: [['CUSTOM USER AGENT']], + customUserAgent, }, }) - assert.strictEqual(service.config.userAgent[0][0], 'CUSTOM USER AGENT') + assert.ok(service.config.customUserAgent) + const pairs = service.config.customUserAgent as [string, string][] + assert.strictEqual(pairs[0][0], 'CUSTOM-USER-AGENT') + assert.strictEqual(pairs[0][1], '1.0.0') }) it('injects http client into handler', function () { @@ -194,6 +269,41 @@ describe('AwsClientBuilderV3', function () { assert.notStrictEqual(firstClient.id, secondClient.id) assert.strictEqual(firstClient.id, thirdClient.id) }) + + it('recreates client when context endpoint URL changes', async function () { + const contextCredentials = { + credentials: {} as any, + credentialsId: 'test', + accountId: '123456789012', + endpointUrl: 'https://endpoint1.example.com', + } + const contextWithEndpoint = new FakeAwsContext({ + contextCredentials, + }) + + const builder = new AWSClientBuilderV3(contextWithEndpoint) + const firstClient = builder.getAwsService({ serviceClient: TestClient }) + // set different endpointUrl + await contextWithEndpoint.setCredentials({ + ...contextCredentials, + endpointUrl: 'https://enpdoint2.example.com', + }) + const secondClient = builder.getAwsService({ serviceClient: TestClient }) + // no endpointUrl + await contextWithEndpoint.setCredentials({ ...contextCredentials, endpointUrl: undefined }) + const thirdClient = builder.getAwsService({ serviceClient: TestClient }) + // use the same endpointUrl again + await contextWithEndpoint.setCredentials({ ...contextCredentials }) + const fourthClient = builder.getAwsService({ serviceClient: TestClient }) + + // Different endpoint URLs should create different clients + assert.notStrictEqual(firstClient.id, secondClient.id) + assert.notStrictEqual(firstClient.id, thirdClient.id) + assert.notStrictEqual(secondClient.id, thirdClient.id) + + // Same endpoint URL should create same client + assert.strictEqual(firstClient.id, fourthClient.id) + }) }) describe('middlewareStack', function () { @@ -283,6 +393,28 @@ describe('AwsClientBuilderV3', function () { assert.strictEqual(newArgs.request.hostname, 'testHost') assert.strictEqual(newArgs.request.path, 'testPath') }) + + it('captures HTTP response headers and attaches to output', async function () { + const testHeaders = { + 'x-custom-header': 'test-value', + 'content-type': 'application/json', + } + response.response = { + statusCode: 200, + headers: testHeaders, + } as any + + const service = builder.createAwsService({ serviceClient: Client }) + // Verify middleware stack exists + const middlewareStack = service.middlewareStack as any + assert.ok(middlewareStack, 'Middleware stack should exist') + + // Verify the middlewareStack has the expected structure + // The captureHeadersMiddleware is added in the awsClientBuilderV3 implementation + // It should be present in the deserialize step + assert.ok(typeof middlewareStack.add === 'function', 'Middleware stack should have add method') + assert.ok(typeof middlewareStack.use === 'function', 'Middleware stack should have use method') + }) }) describe('clientCredentials', function () { diff --git a/packages/core/src/test/shared/clients/defaultIotClient.test.ts b/packages/core/src/test/shared/clients/defaultIotClient.test.ts index a42e6a691af..01cd2740f6a 100644 --- a/packages/core/src/test/shared/clients/defaultIotClient.test.ts +++ b/packages/core/src/test/shared/clients/defaultIotClient.test.ts @@ -4,16 +4,91 @@ */ import assert from 'assert' -import { AWSError, Request, Iot, Endpoint, Config } from 'aws-sdk' +import { ServiceException } from '@smithy/smithy-client' +import { + AttachPolicyCommand, + AttachPolicyRequest, + AttachThingPrincipalCommand, + AttachThingPrincipalRequest, + CreateKeysAndCertificateCommand, + CreateKeysAndCertificateRequest, + CreateKeysAndCertificateResponse, + CreatePolicyCommand, + CreatePolicyRequest, + CreatePolicyResponse, + CreatePolicyVersionCommand, + CreatePolicyVersionRequest, + CreatePolicyVersionResponse, + CreateThingCommand, + CreateThingResponse, + DeleteCertificateCommand, + DeleteCertificateRequest, + DeletePolicyCommand, + DeletePolicyRequest, + DeletePolicyVersionCommand, + DeletePolicyVersionRequest, + DeleteThingCommand, + DeleteThingRequest, + DeleteThingResponse, + DescribeCertificateCommand, + DescribeCertificateRequest, + DescribeCertificateResponse, + DescribeEndpointCommand, + DescribeEndpointRequest, + DescribeEndpointResponse, + DetachPolicyCommand, + DetachPolicyRequest, + DetachThingPrincipalCommand, + DetachThingPrincipalRequest, + GetPolicyVersionCommand, + GetPolicyVersionRequest, + GetPolicyVersionResponse, + IoTClient, + IoTClientResolvedConfig, + ListCertificatesCommand, + ListCertificatesRequest, + ListCertificatesResponse, + ListPoliciesCommand, + ListPoliciesRequest, + ListPoliciesResponse, + ListPolicyVersionsCommand, + ListPolicyVersionsRequest, + ListPolicyVersionsResponse, + ListPrincipalPoliciesCommand, + ListPrincipalPoliciesRequest, + ListPrincipalThingsCommand, + ListPrincipalThingsRequest, + ListPrincipalThingsResponse, + ListTargetsForPolicyCommand, + ListTargetsForPolicyRequest, + ListTargetsForPolicyResponse, + ListThingPrincipalsCommand, + ListThingPrincipalsRequest, + ListThingPrincipalsResponse, + ListThingsCommand, + ListThingsRequest, + ListThingsResponse, + PolicyVersion, + ServiceInputTypes, + ServiceOutputTypes, + SetDefaultPolicyVersionCommand, + SetDefaultPolicyVersionRequest, + UpdateCertificateCommand, + UpdateCertificateRequest, +} from '@aws-sdk/client-iot' import { DefaultIotClient, ListThingCertificatesResponse } from '../../../shared/clients/iotClient' -import { Stub, stub } from '../../utilities/stubber' -import sinon from 'sinon' +import { AwsStub, mockClient } from 'aws-sdk-client-mock' -class FakeAwsError extends Error { +class FakeServiceException extends ServiceException { public region: string = 'us-west-2' public constructor(message: string) { - super(message) + super({ + name: 'FakeServiceException', + $fault: 'client', + $metadata: {}, + message, + }) } } @@ -26,55 +101,33 @@ describe('DefaultIotClient', function () { const marker = nextToken const maxResults = 10 const pageSize = maxResults - let mockIot: Stub + let mockIot: AwsStub beforeEach(function () { - mockIot = stub(Iot, { - config: stub(Config), - apiVersions: [], - endpoint: stub(Endpoint, { - host: '', - hostname: '', - href: '', - port: 0, - protocol: '', - }), - }) + mockIot = mockClient(IoTClient) }) - const error: AWSError = new FakeAwsError('Expected failure') as AWSError - - function success(output?: T): Request { - return { - promise: () => Promise.resolve(output), - } as Request - } - - function failure(): Request { - return { - promise: () => Promise.reject(error), - } as Request - } + const error: ServiceException = new FakeServiceException('Expected failure') as ServiceException function createClient({ regionCode = region }: { regionCode?: string } = {}): DefaultIotClient { - return new DefaultIotClient(regionCode, () => Promise.resolve(mockIot)) + return new DefaultIotClient(regionCode, () => new IoTClient()) } /* Functions that create or retrieve resources. */ describe('createThing', function () { - const expectedResponse: Iot.CreateThingResponse = { thingName: thingName, thingArn: 'arn' } + const expectedResponse: CreateThingResponse = { thingName: thingName, thingArn: 'arn' } it('creates a thing', async function () { - mockIot.createThing.returns(success(expectedResponse)) + mockIot.on(CreateThingCommand).resolves(expectedResponse) const response = await createClient().createThing({ thingName }) - assert(mockIot.createThing.calledOnceWithExactly) + assert.strictEqual(mockIot.commandCalls(CreateThingCommand).length, 1) assert.deepStrictEqual(response, expectedResponse) }) it('throws an Error on failure', async function () { - mockIot.createThing.returns(failure()) + mockIot.on(CreateThingCommand).rejects(error) await assert.rejects(createClient().createThing({ thingName }), error) }) @@ -82,8 +135,8 @@ describe('DefaultIotClient', function () { describe('createCertificateAndKeys', function () { const certificateId = 'cert1' - const input: Iot.CreateKeysAndCertificateRequest = { setAsActive: undefined } - const expectedResponse: Iot.CreateKeysAndCertificateResponse = { + const input: CreateKeysAndCertificateRequest = { setAsActive: undefined } + const expectedResponse: CreateKeysAndCertificateResponse = { certificateId, certificateArn: 'arn', certificatePem: 'pem', @@ -91,7 +144,7 @@ describe('DefaultIotClient', function () { } it('creates Certificate and Key Pair', async function () { - mockIot.createKeysAndCertificate.returns(success(expectedResponse)) + mockIot.on(CreateKeysAndCertificateCommand).resolves(expectedResponse) const response = await createClient().createCertificateAndKeys(input) @@ -99,36 +152,37 @@ describe('DefaultIotClient', function () { }) it('throws an Error on failure', async function () { - mockIot.createKeysAndCertificate.returns(failure()) + mockIot.on(CreateKeysAndCertificateCommand).rejects(error) await assert.rejects(createClient().createCertificateAndKeys(input), error) }) }) describe('getEndpoint', function () { - const input: Iot.DescribeEndpointRequest = { endpointType: 'iot:Data-ATS' } + const input: DescribeEndpointRequest = { endpointType: 'iot:Data-ATS' } const endpointAddress = 'address' - const describeResponse: Iot.DescribeEndpointResponse = { endpointAddress } + const describeResponse: DescribeEndpointResponse = { endpointAddress } it('gets endpoint', async function () { - mockIot.describeEndpoint.returns(success(describeResponse)) + mockIot.on(DescribeEndpointCommand).resolves(describeResponse) const response = await createClient().getEndpoint() - mockIot.describeEndpoint.calledOnceWithExactly(sinon.match(input)) + assert.strictEqual(mockIot.commandCalls(DescribeEndpointCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(DescribeEndpointCommand)[0].args[0].input, input) assert.deepStrictEqual(response, endpointAddress) }) it('throws an Error on failure', async function () { - mockIot.describeEndpoint.returns(failure()) + mockIot.on(DescribeEndpointCommand).rejects(error) await assert.rejects(createClient().getEndpoint(), error) }) }) describe('getPolicyVersion', function () { - const input: Iot.GetPolicyVersionRequest = { policyName, policyVersionId: '1' } - const expectedResponse: Iot.GetPolicyVersionResponse = { + const input: GetPolicyVersionRequest = { policyName, policyVersionId: '1' } + const expectedResponse: GetPolicyVersionResponse = { policyName, policyDocument, policyArn: 'arn1', @@ -136,7 +190,7 @@ describe('DefaultIotClient', function () { } it('gets policy document for version', async function () { - mockIot.getPolicyVersion.returns(success(expectedResponse)) + mockIot.on(GetPolicyVersionCommand).resolves(expectedResponse) const response = await createClient().getPolicyVersion(input) @@ -144,7 +198,7 @@ describe('DefaultIotClient', function () { }) it('throws an Error on failure', async function () { - mockIot.getPolicyVersion.returns(failure()) + mockIot.on(GetPolicyVersionCommand).rejects(error) await assert.rejects(createClient().getPolicyVersion(input), error) }) @@ -153,18 +207,19 @@ describe('DefaultIotClient', function () { /* Functions that return void .*/ describe('deleteThing', function () { - const input: Iot.DeleteThingRequest = { thingName } + const input: DeleteThingRequest = { thingName } it('deletes a thing', async function () { - mockIot.deleteThing.returns(success({} as Iot.DeleteThingResponse)) + mockIot.on(DeleteThingCommand).resolves({} as DeleteThingResponse) await createClient().deleteThing({ thingName }) - assert(mockIot.deleteThing.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(DeleteThingCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(DeleteThingCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.deleteThing.returns(failure()) + mockIot.on(DeleteThingCommand).rejects(error) await assert.rejects(createClient().deleteThing({ thingName }), error) }) @@ -172,18 +227,19 @@ describe('DefaultIotClient', function () { describe('deleteCertificate', function () { const certificateId = 'cert1' - const input: Iot.DeleteCertificateRequest = { certificateId, forceDelete: undefined } + const input: DeleteCertificateRequest = { certificateId, forceDelete: undefined } it('deletes a certificate', async function () { - mockIot.deleteCertificate.returns(success()) + mockIot.on(DeleteCertificateCommand).resolves({}) await createClient().deleteCertificate(input) - assert(mockIot.deleteCertificate.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(DeleteCertificateCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(DeleteCertificateCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.deleteCertificate.returns(failure()) + mockIot.on(DeleteCertificateCommand).rejects(error) await assert.rejects(createClient().deleteCertificate(input), error) }) @@ -191,182 +247,192 @@ describe('DefaultIotClient', function () { describe('updateCertificate', function () { const certificateId = 'cert1' - const input: Iot.UpdateCertificateRequest = { certificateId, newStatus: 'ACTIVE' } + const input: UpdateCertificateRequest = { certificateId, newStatus: 'ACTIVE' } it('updates a certificate', async function () { - mockIot.updateCertificate.returns(success()) + mockIot.on(UpdateCertificateCommand).resolves({}) await createClient().updateCertificate(input) - assert(mockIot.updateCertificate.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(UpdateCertificateCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(UpdateCertificateCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.updateCertificate.returns(failure()) + mockIot.on(UpdateCertificateCommand).rejects(error) await assert.rejects(createClient().updateCertificate(input), error) }) }) describe('attachThingPrincipal', function () { - const input: Iot.AttachThingPrincipalRequest = { thingName, principal: 'arn1' } + const input: AttachThingPrincipalRequest = { thingName, principal: 'arn1' } it('attaches a certificate to a Thing', async function () { - mockIot.attachThingPrincipal.returns(success()) + mockIot.on(AttachThingPrincipalCommand).resolves({}) await createClient().attachThingPrincipal(input) - assert(mockIot.attachThingPrincipal.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(AttachThingPrincipalCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(AttachThingPrincipalCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.attachThingPrincipal.returns(failure()) + mockIot.on(AttachThingPrincipalCommand).rejects(error) await assert.rejects(createClient().attachThingPrincipal(input), error) }) }) describe('detachThingPrincipal', function () { - const input: Iot.DetachThingPrincipalRequest = { thingName, principal: 'arn1' } + const input: DetachThingPrincipalRequest = { thingName, principal: 'arn1' } it('detaches a certificate from a Thing', async function () { - mockIot.detachThingPrincipal.returns(success()) + mockIot.on(DetachThingPrincipalCommand).resolves({}) await createClient().detachThingPrincipal(input) - assert(mockIot.detachThingPrincipal.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(DetachThingPrincipalCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(DetachThingPrincipalCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.detachThingPrincipal.returns(failure()) + mockIot.on(DetachThingPrincipalCommand).rejects(error) await assert.rejects(createClient().detachThingPrincipal(input), error) }) }) describe('attachPolicy', function () { - const input: Iot.AttachPolicyRequest = { policyName, target: 'arn1' } + const input: AttachPolicyRequest = { policyName, target: 'arn1' } it('attaches a policy to a certificate', async function () { - mockIot.attachPolicy.returns(success()) + mockIot.on(AttachPolicyCommand).resolves({}) await createClient().attachPolicy(input) - assert(mockIot.attachPolicy.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(AttachPolicyCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(AttachPolicyCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.attachPolicy.returns(failure()) + mockIot.on(AttachPolicyCommand).rejects(error) await assert.rejects(createClient().attachPolicy(input), error) }) }) describe('detachPolicy', function () { - const input: Iot.DetachPolicyRequest = { policyName, target: 'arn1' } + const input: DetachPolicyRequest = { policyName, target: 'arn1' } it('detaches a policy from a certificate', async function () { - mockIot.detachPolicy.returns(success()) + mockIot.on(DetachPolicyCommand).resolves({}) await createClient().detachPolicy(input) - assert(mockIot.detachPolicy.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(DetachPolicyCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(DetachPolicyCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.detachPolicy.returns(failure()) + mockIot.on(DetachPolicyCommand).rejects(error) await assert.rejects(createClient().detachPolicy(input), error) }) }) describe('createPolicy', function () { - const input: Iot.CreatePolicyRequest = { policyName, policyDocument } - const expectedResponse: Iot.CreatePolicyResponse = { policyName, policyDocument, policyArn: 'arn1' } + const input: CreatePolicyRequest = { policyName, policyDocument } + const expectedResponse: CreatePolicyResponse = { policyName, policyDocument, policyArn: 'arn1' } it('creates a policy from a document', async function () { - mockIot.createPolicy.returns(success(expectedResponse)) + mockIot.on(CreatePolicyCommand).resolves(expectedResponse) await createClient().createPolicy(input) - assert(mockIot.createPolicy.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(CreatePolicyCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(CreatePolicyCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.createPolicy.returns(failure()) + mockIot.on(CreatePolicyCommand).rejects(error) await assert.rejects(createClient().createPolicy(input), error) }) }) describe('deletePolicy', function () { - const input: Iot.DeletePolicyRequest = { policyName } + const input: DeletePolicyRequest = { policyName } it('deletes a policy', async function () { - mockIot.deletePolicy.returns(success()) + mockIot.on(DeletePolicyCommand).resolves({}) await createClient().deletePolicy(input) - assert(mockIot.deletePolicy.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(DeletePolicyCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(DeletePolicyCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.deletePolicy.returns(failure()) + mockIot.on(DeletePolicyCommand).rejects(error) await assert.rejects(createClient().deletePolicy(input), error) }) }) describe('createPolicyVersion', function () { - const input: Iot.CreatePolicyVersionRequest = { policyName, policyDocument } - const expectedResponse: Iot.CreatePolicyVersionResponse = { policyDocument, policyArn: 'arn1' } + const input: CreatePolicyVersionRequest = { policyName, policyDocument } + const expectedResponse: CreatePolicyVersionResponse = { policyDocument, policyArn: 'arn1' } it('creates a policy version from a document', async function () { - mockIot.createPolicyVersion.returns(success(expectedResponse)) + mockIot.on(CreatePolicyVersionCommand).resolves(expectedResponse) await createClient().createPolicyVersion(input) - assert(mockIot.createPolicyVersion.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(CreatePolicyVersionCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(CreatePolicyVersionCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.createPolicyVersion.returns(failure()) + mockIot.on(CreatePolicyVersionCommand).rejects(error) await assert.rejects(createClient().createPolicyVersion(input), error) }) }) describe('deletePolicyVersion', function () { - const input: Iot.DeletePolicyVersionRequest = { policyName, policyVersionId: '1' } + const input: DeletePolicyVersionRequest = { policyName, policyVersionId: '1' } it('deletes a policy version', async function () { - mockIot.deletePolicyVersion.returns(success()) + mockIot.on(DeletePolicyVersionCommand).resolves({}) await createClient().deletePolicyVersion(input) - assert(mockIot.deletePolicyVersion.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(DeletePolicyVersionCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(DeletePolicyVersionCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.deletePolicyVersion.returns(failure()) + mockIot.on(DeletePolicyVersionCommand).rejects(error) await assert.rejects(createClient().deletePolicyVersion(input), error) }) }) describe('setDefaultPolicyVersion', function () { - const input: Iot.SetDefaultPolicyVersionRequest = { policyName, policyVersionId: '1' } + const input: SetDefaultPolicyVersionRequest = { policyName, policyVersionId: '1' } it('deletes a policy version', async function () { - mockIot.setDefaultPolicyVersion.returns(success()) + mockIot.on(SetDefaultPolicyVersionCommand).resolves({}) await createClient().setDefaultPolicyVersion(input) - assert(mockIot.setDefaultPolicyVersion.calledOnceWithExactly(sinon.match(input))) + assert.strictEqual(mockIot.commandCalls(SetDefaultPolicyVersionCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(SetDefaultPolicyVersionCommand)[0].args[0].input, input) }) it('throws an Error on failure', async function () { - mockIot.setDefaultPolicyVersion.returns(failure()) + mockIot.on(SetDefaultPolicyVersionCommand).rejects(error) await assert.rejects(createClient().setDefaultPolicyVersion(input), error) }) @@ -375,11 +441,11 @@ describe('DefaultIotClient', function () { // /* Functions that list resources. describe('listThings', function () { - const input: Iot.ListThingsRequest = { maxResults, nextToken } - const expectedResponse: Iot.ListThingsResponse = { things: [{ thingName: 'thing1' }], nextToken } + const input: ListThingsRequest = { maxResults, nextToken } + const expectedResponse: ListThingsResponse = { things: [{ thingName: 'thing1' }], nextToken } it('lists things', async function () { - mockIot.listThings.returns(success(expectedResponse)) + mockIot.on(ListThingsCommand).resolves(expectedResponse) const response = await createClient().listThings(input) @@ -387,21 +453,21 @@ describe('DefaultIotClient', function () { }) it('throws an Error on failure', async function () { - mockIot.listThings.returns(failure()) + mockIot.on(ListThingsCommand).rejects(error) await assert.rejects(createClient().listThings(input), error) }) }) describe('listCertificates', function () { - const input: Iot.ListCertificatesRequest = { pageSize, marker, ascendingOrder: undefined } - const expectedResponse: Iot.ListCertificatesResponse = { + const input: ListCertificatesRequest = { pageSize, marker, ascendingOrder: undefined } + const expectedResponse: ListCertificatesResponse = { certificates: [{ certificateId: 'cert1' }], nextMarker: marker, } it('lists certificates', async function () { - mockIot.listCertificates.returns(success(expectedResponse)) + mockIot.on(ListCertificatesCommand).resolves(expectedResponse) const response = await createClient().listCertificates(input) @@ -409,7 +475,7 @@ describe('DefaultIotClient', function () { }) it('throws an Error on failure', async function () { - mockIot.listCertificates.returns(failure()) + mockIot.on(ListCertificatesCommand).rejects(error) await assert.rejects(createClient().listCertificates(input), error) }) @@ -418,11 +484,11 @@ describe('DefaultIotClient', function () { describe('listThingCertificates', function () { const certificateId = 'cert1' const certArn = 'arn:aws:iot:us-west-2:0123456789:cert/cert1' - const input: Iot.ListThingPrincipalsRequest = { thingName, maxResults, nextToken } - const principalsResponse: Iot.ListThingPrincipalsResponse = { principals: [certArn], nextToken } + const input: ListThingPrincipalsRequest = { thingName, maxResults, nextToken } + const principalsResponse: ListThingPrincipalsResponse = { principals: [certArn], nextToken } - const describeInput: Iot.DescribeCertificateRequest = { certificateId } - const describeResponse: Iot.DescribeCertificateResponse = { + const describeInput: DescribeCertificateRequest = { certificateId } + const describeResponse: DescribeCertificateResponse = { certificateDescription: { certificateId, certificateArn: certArn }, } @@ -432,36 +498,37 @@ describe('DefaultIotClient', function () { } it('lists certificates', async function () { - mockIot.listThingPrincipals.returns(success(principalsResponse)) - mockIot.describeCertificate.returns(success(describeResponse)) + mockIot.on(ListThingPrincipalsCommand).resolves(principalsResponse) + mockIot.on(DescribeCertificateCommand).resolves(describeResponse) const response = await createClient().listThingCertificates(input) - mockIot.describeCertificate.calledOnceWithExactly(sinon.match(describeInput)) + assert.strictEqual(mockIot.commandCalls(DescribeCertificateCommand).length, 1) + assert.deepStrictEqual(mockIot.commandCalls(DescribeCertificateCommand)[0].args[0].input, describeInput) assert.deepStrictEqual(response, expectedResponse) }) it('throws an Error when certificate listing fails', async function () { - mockIot.listThingPrincipals.returns(failure()) + mockIot.on(ListThingPrincipalsCommand).rejects(error) await assert.rejects(createClient().listThingCertificates(input), error) }) it('throws an Error when certificate description fails', async function () { - mockIot.listThingPrincipals.returns(success(principalsResponse)) - mockIot.describeCertificate.returns(failure()) + mockIot.on(ListThingPrincipalsCommand).resolves(principalsResponse) + mockIot.on(DescribeCertificateCommand).rejects(error) await assert.rejects(createClient().listThingCertificates(input), error) }) }) describe('listThingsForCert', function () { - const input: Iot.ListPrincipalThingsRequest = { principal: 'arn1', maxResults, nextToken } - const listResponse: Iot.ListPrincipalThingsResponse = { things: [thingName], nextToken } + const input: ListPrincipalThingsRequest = { principal: 'arn1', maxResults, nextToken } + const listResponse: ListPrincipalThingsResponse = { things: [thingName], nextToken } const expectedResponse = [thingName] it('lists things', async function () { - mockIot.listPrincipalThings.returns(success(listResponse)) + mockIot.on(ListPrincipalThingsCommand).resolves(listResponse) const response = await createClient().listThingsForCert(input) @@ -469,18 +536,18 @@ describe('DefaultIotClient', function () { }) it('throws an Error on failure', async function () { - mockIot.listPrincipalThings.returns(failure()) + mockIot.on(ListPrincipalThingsCommand).rejects(error) await assert.rejects(createClient().listThingsForCert(input), error) }) }) describe('listPolicies', function () { - const input: Iot.ListPoliciesRequest = { pageSize, marker, ascendingOrder: undefined } - const expectedResponse: Iot.ListPoliciesResponse = { policies: [{ policyName }], nextMarker: marker } + const input: ListPoliciesRequest = { pageSize, marker, ascendingOrder: undefined } + const expectedResponse: ListPoliciesResponse = { policies: [{ policyName }], nextMarker: marker } it('lists policies', async function () { - mockIot.listPolicies.returns(success(expectedResponse)) + mockIot.on(ListPoliciesCommand).resolves(expectedResponse) const response = await createClient().listPolicies(input) @@ -488,23 +555,23 @@ describe('DefaultIotClient', function () { }) it('throws an Error on failure', async function () { - mockIot.listPolicies.returns(failure()) + mockIot.on(ListPoliciesCommand).rejects(error) await assert.rejects(createClient().listPolicies(input), error) }) }) describe('listPrincipalPolicies', function () { - const input: Iot.ListPrincipalPoliciesRequest = { + const input: ListPrincipalPoliciesRequest = { pageSize, marker, ascendingOrder: undefined, principal: 'arn1', } - const expectedResponse: Iot.ListPoliciesResponse = { policies: [{ policyName }], nextMarker: marker } + const expectedResponse: ListPoliciesResponse = { policies: [{ policyName }], nextMarker: marker } it('lists policies for certificate', async function () { - mockIot.listPrincipalPolicies.returns(success(expectedResponse)) + mockIot.on(ListPrincipalPoliciesCommand).resolves(expectedResponse) const response = await createClient().listPrincipalPolicies(input) @@ -512,7 +579,7 @@ describe('DefaultIotClient', function () { }) it('throws an Error on failure', async function () { - mockIot.listPrincipalPolicies.returns(failure()) + mockIot.on(ListPrincipalPoliciesCommand).rejects(error) await assert.rejects(createClient().listPrincipalPolicies(input), error) }) @@ -520,11 +587,11 @@ describe('DefaultIotClient', function () { describe('listPolicyTargets', function () { const targets = ['arn1', 'arn2'] - const input: Iot.ListTargetsForPolicyRequest = { policyName, pageSize, marker } - const listResponse: Iot.ListTargetsForPolicyResponse = { targets, nextMarker: marker } + const input: ListTargetsForPolicyRequest = { policyName, pageSize, marker } + const listResponse: ListTargetsForPolicyResponse = { targets, nextMarker: marker } it('lists certificates', async function () { - mockIot.listTargetsForPolicy.returns(success(listResponse)) + mockIot.on(ListTargetsForPolicyCommand).resolves(listResponse) const response = await createClient().listPolicyTargets(input) @@ -532,20 +599,20 @@ describe('DefaultIotClient', function () { }) it('throws an Error on failure', async function () { - mockIot.listTargetsForPolicy.returns(failure()) + mockIot.on(ListTargetsForPolicyCommand).rejects(error) await assert.rejects(createClient().listPolicyTargets(input), error) }) }) describe('listPolicyVersions', function () { - const input: Iot.ListPolicyVersionsRequest = { policyName } - const expectedVersion1: Iot.PolicyVersion = { versionId: '1' } - const expectedVersion2: Iot.PolicyVersion = { versionId: '2' } - const listResponse: Iot.ListPolicyVersionsResponse = { policyVersions: [expectedVersion1, expectedVersion2] } + const input: ListPolicyVersionsRequest = { policyName } + const expectedVersion1: PolicyVersion = { versionId: '1' } + const expectedVersion2: PolicyVersion = { versionId: '2' } + const listResponse: ListPolicyVersionsResponse = { policyVersions: [expectedVersion1, expectedVersion2] } it('lists policy versions', async function () { - mockIot.listPolicyVersions.returns(success(listResponse)) + mockIot.on(ListPolicyVersionsCommand).resolves(listResponse) const iterable = createClient().listPolicyVersions(input) const responses = [] @@ -561,7 +628,7 @@ describe('DefaultIotClient', function () { }) it('throws an Error on iterate failure', async function () { - mockIot.listPolicyVersions.returns(failure()) + mockIot.on(ListPolicyVersionsCommand).rejects(error) const iterable = createClient().listPolicyVersions(input) await assert.rejects(iterable.next(), error) diff --git a/packages/core/src/test/shared/clients/defaultRedshiftClient.test.ts b/packages/core/src/test/shared/clients/defaultRedshiftClient.test.ts index 5a7241aefd6..77e2fced64b 100644 --- a/packages/core/src/test/shared/clients/defaultRedshiftClient.test.ts +++ b/packages/core/src/test/shared/clients/defaultRedshiftClient.test.ts @@ -3,24 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Redshift, RedshiftData, RedshiftServerless, AWSError, Request } from 'aws-sdk' +import { ClustersMessage, RedshiftClient, DescribeClustersCommand } from '@aws-sdk/client-redshift' +import { + ListDatabasesResponse, + ListSchemasResponse, + RedshiftDataClient, + ListDatabasesCommand, + ListSchemasCommand, +} from '@aws-sdk/client-redshift-data' +import { + ListWorkgroupsResponse, + RedshiftServerlessClient, + ListWorkgroupsCommand, +} from '@aws-sdk/client-redshift-serverless' import { DefaultRedshiftClient } from '../../../shared/clients/redshiftClient' import assert = require('assert') import { ConnectionParams, ConnectionType, RedshiftWarehouseType } from '../../../awsService/redshift/models/models' -import sinon = require('sinon') - -function success(output?: T): Request { - return { - promise: () => Promise.resolve(output), - } as Request -} +import { mockClient, AwsClientStub } from 'aws-sdk-client-mock' const nextToken = 'testNextToken' describe('DefaultRedshiftClient', function () { let defaultRedshiftClient: DefaultRedshiftClient - let mockRedshift: Redshift - let mockRedshiftData: RedshiftData - let mockRedshiftServerless: RedshiftServerless + let mockRedshift: AwsClientStub + let mockRedshiftData: AwsClientStub + let mockRedshiftServerless: AwsClientStub const clusterIdentifier = 'ClusterId' const workgroupName = 'Workgroup' const dbName = 'DB' @@ -38,116 +44,127 @@ describe('DefaultRedshiftClient', function () { workgroupName, RedshiftWarehouseType.SERVERLESS ) - let sandbox: sinon.SinonSandbox - - before(function () { - sandbox = sinon.createSandbox() - }) - beforeEach(function () { - mockRedshift = {} - mockRedshiftData = {} - mockRedshiftServerless = {} + mockRedshift = mockClient(RedshiftClient) + mockRedshiftData = mockClient(RedshiftDataClient) + mockRedshiftServerless = mockClient(RedshiftServerlessClient) defaultRedshiftClient = new DefaultRedshiftClient( 'us-east-1', - async (r) => Promise.resolve(mockRedshiftData), - async (r) => Promise.resolve(mockRedshift), - async (r) => Promise.resolve(mockRedshiftServerless) + // @ts-expect-error + () => mockRedshiftData, + () => mockRedshift, + () => mockRedshiftServerless ) }) + afterEach(function () { + mockRedshift.reset() + mockRedshiftData.reset() + mockRedshiftServerless.reset() + }) + describe('describeProvisionedClusters', function () { - const expectedResponse = { Clusters: [] } as Redshift.ClustersMessage - let describeClustersStub: sinon.SinonStub + const expectedResponse = { Clusters: [] } as ClustersMessage beforeEach(function () { - describeClustersStub = sandbox.stub() - mockRedshift.describeClusters = describeClustersStub - describeClustersStub.returns(success(expectedResponse)) + mockRedshift.on(DescribeClustersCommand).resolves(expectedResponse) }) it('without nextToken should not set Marker', async () => { const response = await defaultRedshiftClient.describeProvisionedClusters() - describeClustersStub.alwaysCalledWith({ Marker: undefined, MaxRecords: 20 }) + const calls = mockRedshift.commandCalls(DescribeClustersCommand) + assert.strictEqual(calls.length, 1) + assert.deepStrictEqual(calls[0].args[0].input, { Marker: undefined, MaxRecords: 20 }) assert.deepStrictEqual(response.Clusters, []) }) it('with nextToken should set the Marker', async () => { const response = await defaultRedshiftClient.describeProvisionedClusters(nextToken) - describeClustersStub.alwaysCalledWith({ Marker: nextToken, MaxRecords: 20 }) + const calls = mockRedshift.commandCalls(DescribeClustersCommand) + assert.strictEqual(calls.length, 1) + assert.deepStrictEqual(calls[0].args[0].input, { Marker: nextToken, MaxRecords: 20 }) assert.deepStrictEqual(response.Clusters, []) }) }) describe('listServerlessWorkgroups', function () { - const expectedResponse = { workgroups: [] } as RedshiftServerless.ListWorkgroupsResponse - let listServerlessWorkgroupsStub: sinon.SinonStub + const expectedResponse = { workgroups: [] } as ListWorkgroupsResponse + beforeEach(function () { - listServerlessWorkgroupsStub = sandbox.stub() - mockRedshiftServerless.listWorkgroups = listServerlessWorkgroupsStub - listServerlessWorkgroupsStub.returns(success(expectedResponse)) + mockRedshiftServerless.on(ListWorkgroupsCommand).resolves(expectedResponse) }) it('without nextToken should not set nextToken in RedshiftServerless request', async () => { const response = await defaultRedshiftClient.listServerlessWorkgroups() - listServerlessWorkgroupsStub.alwaysCalledWith({ nextToken: undefined, maxResults: 20 }) + const calls = mockRedshiftServerless.commandCalls(ListWorkgroupsCommand) + assert.strictEqual(calls.length, 1) + assert.deepStrictEqual(calls[0].args[0].input, { nextToken: undefined, maxResults: 20 }) assert.deepStrictEqual(response.workgroups, []) }) it('with nextToken should set nextToken in RedshiftServerless request', async () => { const response = await defaultRedshiftClient.listServerlessWorkgroups(nextToken) - listServerlessWorkgroupsStub.alwaysCalledWith({ nextToken: nextToken, maxResults: 20 }) + const calls = mockRedshiftServerless.commandCalls(ListWorkgroupsCommand) + assert.strictEqual(calls.length, 1) + assert.deepStrictEqual(calls[0].args[0].input, { nextToken: nextToken, maxResults: 20 }) assert.deepStrictEqual(response.workgroups, []) }) }) describe('listDatabases', function () { - const expectedResponse = { Databases: [] } as RedshiftData.ListDatabasesResponse - let listDatabasesStub: sinon.SinonStub + const expectedResponse = { Databases: [] } as ListDatabasesResponse + beforeEach(function () { - listDatabasesStub = sandbox.stub() - mockRedshiftData.listDatabases = listDatabasesStub - listDatabasesStub.returns(success(expectedResponse)) + mockRedshiftData.on(ListDatabasesCommand).resolves(expectedResponse) }) + it('should list databases for provisioned clusters', async () => { const response = await defaultRedshiftClient.listDatabases(provisionedDbUserAndPasswordParams) - listDatabasesStub.alwaysCalledWith({ - ClusterIdentifier: clusterIdentifier, - Database: dbName, - DbUser: dbUsername, - }) + const calls = mockRedshiftData.commandCalls(ListDatabasesCommand) + assert.strictEqual(calls.length, 1) + const input = calls[0].args[0].input + assert.strictEqual(input.ClusterIdentifier, clusterIdentifier) + assert.strictEqual(input.Database, dbName) + assert.strictEqual(input.DbUser, dbUsername) assert.deepStrictEqual(response.Databases, []) }) it('should list databases for serverless workgroups', async () => { const response = await defaultRedshiftClient.listDatabases(serverlessFederatedParams) - listDatabasesStub.alwaysCalledWith({ WorkgroupName: workgroupName, Database: dbName }) + const calls = mockRedshiftData.commandCalls(ListDatabasesCommand) + assert.strictEqual(calls.length, 1) + const input = calls[0].args[0].input + assert.strictEqual(input.WorkgroupName, workgroupName) + assert.strictEqual(input.Database, dbName) assert.deepStrictEqual(response.Databases, []) }) }) describe('listSchemas', function () { - const expectedResponse = { Schemas: [] } as RedshiftData.ListSchemasResponse - let listSchemasStub: sinon.SinonStub + const expectedResponse = { Schemas: [] } as ListSchemasResponse + beforeEach(function () { - listSchemasStub = sandbox.stub() - mockRedshiftData.listSchemas = listSchemasStub - listSchemasStub.returns(success(expectedResponse)) + mockRedshiftData.on(ListSchemasCommand).resolves(expectedResponse) }) it('should list schemas for databases in provisioned clusters', async () => { const response = await defaultRedshiftClient.listSchemas(provisionedDbUserAndPasswordParams, dbName) - listSchemasStub.alwaysCalledWith({ - ClusterIdentifier: clusterIdentifier, - Database: dbName, - DbUser: dbUsername, - }) + const calls = mockRedshiftData.commandCalls(ListSchemasCommand) + assert.strictEqual(calls.length, 1) + const input = calls[0].args[0].input + assert.strictEqual(input.ClusterIdentifier, clusterIdentifier) + assert.strictEqual(input.Database, dbName) + assert.strictEqual(input.DbUser, dbUsername) assert.deepStrictEqual(response.Schemas, []) }) it('should list schemas for databases in serverless workgroups', async () => { const response = await defaultRedshiftClient.listSchemas(serverlessFederatedParams, dbName) - listSchemasStub.alwaysCalledWith({ WorkgroupName: workgroupName, Database: dbName }) + const calls = mockRedshiftData.commandCalls(ListSchemasCommand) + assert.strictEqual(calls.length, 1) + const input = calls[0].args[0].input + assert.strictEqual(input.WorkgroupName, workgroupName) + assert.strictEqual(input.Database, dbName) assert.deepStrictEqual(response.Schemas, []) }) }) diff --git a/packages/core/src/test/shared/clients/sagemakerClient.test.ts b/packages/core/src/test/shared/clients/sagemakerClient.test.ts index 888d2222692..f99b8ed82e2 100644 --- a/packages/core/src/test/shared/clients/sagemakerClient.test.ts +++ b/packages/core/src/test/shared/clients/sagemakerClient.test.ts @@ -6,9 +6,12 @@ import * as sinon from 'sinon' import * as assert from 'assert' import { SagemakerClient } from '../../../shared/clients/sagemaker' -import { AppDetails, SpaceDetails, DescribeDomainCommandOutput } from '@aws-sdk/client-sagemaker' +import { AppDetails, SpaceDetails, DescribeDomainCommandOutput, AppType } from '@aws-sdk/client-sagemaker' import { DescribeDomainResponse } from '@amzn/sagemaker-client' import { intoCollection } from '../../../shared/utilities/collectionUtils' +import { ToolkitError } from '../../../shared/errors' +import { getTestWindow } from '../vscode/window' +import { InstanceTypeInsufficientMemoryMessage } from '../../../awsService/sagemaker/constants' describe('SagemakerClient.fetchSpaceAppsAndDomains', function () { const region = 'test-region' @@ -91,10 +94,6 @@ describe('SagemakerClient.fetchSpaceAppsAndDomains', function () { listAppsStub.returns(intoCollection([{ AppName: 'app1', DomainId: 'domain1', SpaceName: 'space1' }])) const [spaceApps] = await client.fetchSpaceAppsAndDomains() - for (const space of spaceApps) { - console.log(space[0]) - console.log(space[1]) - } const spaceAppKey2 = 'domain2__space2' const spaceAppKey3 = 'domain2__space3' @@ -104,121 +103,333 @@ describe('SagemakerClient.fetchSpaceAppsAndDomains', function () { assert.strictEqual(spaceApps.get(spaceAppKey3)?.App, undefined) }) - describe('SagemakerClient.startSpace', function () { - const region = 'test-region' - let client: SagemakerClient - let describeSpaceStub: sinon.SinonStub - let updateSpaceStub: sinon.SinonStub - let waitForSpaceStub: sinon.SinonStub - let createAppStub: sinon.SinonStub - - beforeEach(function () { - client = new SagemakerClient(region) - describeSpaceStub = sinon.stub(client, 'describeSpace') - updateSpaceStub = sinon.stub(client, 'updateSpace') - waitForSpaceStub = sinon.stub(client as any, 'waitForSpaceInService') - createAppStub = sinon.stub(client, 'createApp') - }) + it('filters out unified studio domains when filterSmusDomains is true', async function () { + const [spaceApps] = await client.fetchSpaceAppsAndDomains(undefined, true) - afterEach(function () { - sinon.restore() - }) + assert.strictEqual(spaceApps.size, 3) + assert.ok(!spaceApps.has('domain3__space4')) + }) + + it('includes unified studio domains when filterSmusDomains is false', async function () { + const [spaceApps] = await client.fetchSpaceAppsAndDomains(undefined, false) + + assert.strictEqual(spaceApps.size, 4) + assert.ok(spaceApps.has('domain3__space4')) + }) + + it('handles AccessDeniedException and shows error message', async function () { + sinon.stub(client, 'listSpaceApps').rejects({ name: 'AccessDeniedException' }) + + await assert.rejects(client.fetchSpaceAppsAndDomains()) + + const messages = getTestWindow().shownMessages + assert.ok(messages.some((m) => m.message.includes('AccessDeniedException'))) + }) +}) + +describe('SagemakerClient.listSpaceApps', function () { + const region = 'test-region' + let client: SagemakerClient + + const appDetails: AppDetails[] = [ + { AppName: 'app1', DomainId: 'domain1', SpaceName: 'space1', AppType: AppType.CodeEditor }, + { AppName: 'app2', DomainId: 'domain2', SpaceName: 'space2', AppType: AppType.JupyterLab }, + { AppName: 'app3', DomainId: 'domain2', SpaceName: 'space3', AppType: 'Studio' as any }, + ] + + const spaceDetails: SpaceDetails[] = [ + { SpaceName: 'space1', DomainId: 'domain1' }, + { SpaceName: 'space2', DomainId: 'domain2' }, + { SpaceName: 'space3', DomainId: 'domain2' }, + ] + + beforeEach(function () { + client = new SagemakerClient(region) + sinon.stub(client, 'listApps').returns(intoCollection([appDetails])) + sinon.stub(client, 'listSpaces').returns(intoCollection([spaceDetails])) + }) + + afterEach(function () { + sinon.restore() + }) - it('enables remote access and starts the app', async function () { - describeSpaceStub.resolves({ - SpaceSettings: { - RemoteAccess: 'DISABLED', - AppType: 'CodeEditor', - CodeEditorAppSettings: { - DefaultResourceSpec: { - InstanceType: 'ml.t3.large', - SageMakerImageArn: 'arn:aws:sagemaker:us-west-2:img', - SageMakerImageVersionAlias: '1.0.0', - }, + it('returns space apps with correct mapping', async function () { + const spaceApps = await client.listSpaceApps() + + assert.strictEqual(spaceApps.size, 3) + assert.strictEqual(spaceApps.get('domain1__space1')?.App?.AppName, 'app1') + assert.strictEqual(spaceApps.get('domain2__space2')?.App?.AppName, 'app2') + assert.strictEqual(spaceApps.get('domain2__space3')?.App, undefined) // Studio app filtered out + }) + + it('filters by domain when domainId provided', async function () { + const newClient = new SagemakerClient(region) + const listAppsStub = sinon.stub(newClient, 'listApps').returns(intoCollection([])) + const listSpacesStub = sinon.stub(newClient, 'listSpaces').returns(intoCollection([])) + + await newClient.listSpaceApps('domain1') + + sinon.assert.calledWith(listAppsStub, { DomainIdEquals: 'domain1' }) + sinon.assert.calledWith(listSpacesStub, { DomainIdEquals: 'domain1' }) + }) +}) + +describe('SagemakerClient.listAppForSpace', function () { + const region = 'test-region' + let client: SagemakerClient + let listAppsStub: sinon.SinonStub + + beforeEach(function () { + client = new SagemakerClient(region) + listAppsStub = sinon.stub(client, 'listApps') + }) + + afterEach(function () { + sinon.restore() + }) + + it('returns first app for given domain and space', async function () { + const appDetails: AppDetails[] = [ + { AppName: 'app1', DomainId: 'domain1', SpaceName: 'space1', AppType: AppType.CodeEditor }, + ] + listAppsStub.returns(intoCollection([appDetails])) + + const result = await client.listAppForSpace('domain1', 'space1') + + assert.strictEqual(result?.AppName, 'app1') + sinon.assert.calledWith(listAppsStub, { DomainIdEquals: 'domain1', SpaceNameEquals: 'space1' }) + }) + + it('returns undefined when no apps found', async function () { + listAppsStub.returns(intoCollection([[]])) + + const result = await client.listAppForSpace('domain1', 'space1') + + assert.strictEqual(result, undefined) + }) +}) + +describe('SagemakerClient.waitForAppInService', function () { + const region = 'test-region' + let client: SagemakerClient + let describeAppStub: sinon.SinonStub + + beforeEach(function () { + client = new SagemakerClient(region) + describeAppStub = sinon.stub(client, 'describeApp') + }) + + afterEach(function () { + sinon.restore() + }) + + it('resolves when app reaches InService status', async function () { + describeAppStub.resolves({ Status: 'InService' }) + + await client.waitForAppInService('domain1', 'space1', 'CodeEditor') + + sinon.assert.calledOnce(describeAppStub) + }) + + it('throws error when app status is Failed', async function () { + describeAppStub.resolves({ Status: 'Failed' }) + + await assert.rejects( + client.waitForAppInService('domain1', 'space1', 'CodeEditor'), + /App failed to start. Status: Failed/ + ) + }) + + it('throws error when app status is DeleteFailed', async function () { + describeAppStub.resolves({ Status: 'DeleteFailed' }) + + await assert.rejects( + client.waitForAppInService('domain1', 'space1', 'CodeEditor'), + /App failed to start. Status: DeleteFailed/ + ) + }) + + it('times out after max retries', async function () { + describeAppStub.resolves({ Status: 'Pending' }) + + const sagemakerModule = await import('../../../shared/clients/sagemaker.js') + const originalValue = sagemakerModule.waitForAppConfig.hardTimeoutRetries + sagemakerModule.waitForAppConfig.hardTimeoutRetries = 3 + + try { + await assert.rejects( + client.waitForAppInService('domain1', 'space1', 'CodeEditor'), + /Timed out waiting for app/ + ) + } finally { + sagemakerModule.waitForAppConfig.hardTimeoutRetries = originalValue + } + }) +}) + +describe('SagemakerClient.startSpace', function () { + const region = 'test-region' + let client: SagemakerClient + let describeSpaceStub: sinon.SinonStub + let updateSpaceStub: sinon.SinonStub + let waitForSpaceStub: sinon.SinonStub + let createAppStub: sinon.SinonStub + + beforeEach(function () { + client = new SagemakerClient(region) + describeSpaceStub = sinon.stub(client, 'describeSpace') + updateSpaceStub = sinon.stub(client, 'updateSpace') + waitForSpaceStub = sinon.stub(client as any, 'waitForSpaceInService') + createAppStub = sinon.stub(client, 'createApp') + }) + + afterEach(function () { + sinon.restore() + }) + + it('enables remote access and starts the app', async function () { + describeSpaceStub.resolves({ + SpaceSettings: { + RemoteAccess: 'DISABLED', + AppType: 'CodeEditor', + CodeEditorAppSettings: { + DefaultResourceSpec: { + InstanceType: 'ml.t3.large', + SageMakerImageArn: 'arn:aws:sagemaker:us-west-2:img', + SageMakerImageVersionAlias: '1.0.0', }, }, - }) + }, + }) - updateSpaceStub.resolves({}) - waitForSpaceStub.resolves() - createAppStub.resolves({}) + updateSpaceStub.resolves({}) + waitForSpaceStub.resolves() + createAppStub.resolves({}) - await client.startSpace('my-space', 'my-domain') + await client.startSpace('my-space', 'my-domain') - sinon.assert.calledOnce(updateSpaceStub) - sinon.assert.calledOnce(waitForSpaceStub) - sinon.assert.calledOnce(createAppStub) - }) + sinon.assert.calledOnce(updateSpaceStub) + sinon.assert.calledOnce(waitForSpaceStub) + sinon.assert.calledOnce(createAppStub) + }) - it('skips enabling remote access if already enabled', async function () { - describeSpaceStub.resolves({ - SpaceSettings: { - RemoteAccess: 'ENABLED', - AppType: 'CodeEditor', - CodeEditorAppSettings: { - DefaultResourceSpec: { - InstanceType: 'ml.t3.large', - SageMakerImageArn: 'arn:aws:sagemaker:us-west-2:img', - SageMakerImageVersionAlias: '1.0.0', - }, + it('skips enabling remote access if already enabled', async function () { + describeSpaceStub.resolves({ + SpaceSettings: { + RemoteAccess: 'ENABLED', + AppType: 'CodeEditor', + CodeEditorAppSettings: { + DefaultResourceSpec: { + InstanceType: 'ml.t3.large', + SageMakerImageArn: 'arn:aws:sagemaker:us-west-2:img', + SageMakerImageVersionAlias: '1.0.0', }, }, - }) + }, + }) - createAppStub.resolves({}) + createAppStub.resolves({}) - await client.startSpace('my-space', 'my-domain') + await client.startSpace('my-space', 'my-domain') - sinon.assert.notCalled(updateSpaceStub) - sinon.assert.notCalled(waitForSpaceStub) - sinon.assert.calledOnce(createAppStub) + sinon.assert.notCalled(updateSpaceStub) + sinon.assert.notCalled(waitForSpaceStub) + sinon.assert.calledOnce(createAppStub) + }) + + it('throws error on unsupported app type', async function () { + describeSpaceStub.resolves({ + SpaceSettings: { + RemoteAccess: 'ENABLED', + AppType: 'Studio', + }, }) - it('throws error on unsupported app type', async function () { - describeSpaceStub.resolves({ - SpaceSettings: { - RemoteAccess: 'ENABLED', - AppType: 'Studio', + await assert.rejects(client.startSpace('my-space', 'my-domain'), /Unsupported AppType "Studio"/) + }) + + it('uses fallback resource spec when none provided', async function () { + describeSpaceStub.resolves({ + SpaceSettings: { + RemoteAccess: 'ENABLED', + AppType: 'JupyterLab', + JupyterLabAppSettings: { + DefaultResourceSpec: { + InstanceType: 'ml.t3.large', + }, }, + }, + }) + + createAppStub.resolves({}) + + await client.startSpace('my-space', 'my-domain') + + sinon.assert.calledOnceWithExactly( + createAppStub, + sinon.match.hasNested('ResourceSpec', { + InstanceType: 'ml.t3.large', + SageMakerImageArn: 'arn:aws:sagemaker:us-west-2:542918446943:image/sagemaker-distribution-cpu', + SageMakerImageVersionAlias: '3.2.0', }) + ) + }) - await assert.rejects(client.startSpace('my-space', 'my-domain'), /Unsupported AppType "Studio"/) - }) + it('handles AccessDeniedException gracefully', async function () { + describeSpaceStub.rejects({ name: 'AccessDeniedException', message: 'no access' }) + + await assert.rejects(client.startSpace('my-space', 'my-domain'), /You do not have permission to start spaces/) + }) - it('uses fallback resource spec when none provided', async function () { - describeSpaceStub.resolves({ - SpaceSettings: { - RemoteAccess: 'ENABLED', - AppType: 'JupyterLab', - JupyterLabAppSettings: { - DefaultResourceSpec: { - InstanceType: 'ml.t3.large', - }, + it('prompts user for insufficient memory instance type', async function () { + describeSpaceStub.resolves({ + SpaceName: 'my-space', + SpaceSettings: { + RemoteAccess: 'ENABLED', + AppType: 'CodeEditor', + CodeEditorAppSettings: { + DefaultResourceSpec: { + InstanceType: 'ml.t3.medium', // Insufficient memory type }, }, - }) + }, + }) - createAppStub.resolves({}) + createAppStub.resolves({}) - await client.startSpace('my-space', 'my-domain') + const promise = client.startSpace('my-space', 'my-domain') - sinon.assert.calledOnceWithExactly( - createAppStub, - sinon.match.hasNested('ResourceSpec', { - InstanceType: 'ml.t3.large', - SageMakerImageArn: 'arn:aws:sagemaker:us-west-2:542918446943:image/sagemaker-distribution-cpu', - SageMakerImageVersionAlias: '3.2.0', - }) - ) - }) + // Wait for the error message to appear and select "Restart Space and Connect" + const expectedMessage = InstanceTypeInsufficientMemoryMessage('my-space', 'ml.t3.medium', 'ml.t3.large') + await getTestWindow().waitForMessage(new RegExp(expectedMessage.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))) + getTestWindow().getFirstMessage().selectItem('Restart Space and Connect') - it('handles AccessDeniedException gracefully', async function () { - describeSpaceStub.rejects({ name: 'AccessDeniedException', message: 'no access' }) + await promise + sinon.assert.calledOnce(updateSpaceStub) + sinon.assert.calledOnce(createAppStub) + }) - await assert.rejects( - client.startSpace('my-space', 'my-domain'), - /You do not have permission to start spaces/ - ) + it('throws error when user declines insufficient memory upgrade', async function () { + describeSpaceStub.resolves({ + SpaceName: 'my-space', + SpaceSettings: { + RemoteAccess: 'ENABLED', + AppType: 'CodeEditor', + CodeEditorAppSettings: { + DefaultResourceSpec: { + InstanceType: 'ml.t3.medium', + }, + }, + }, }) + + const promise = client.startSpace('my-space', 'my-domain') + + // Wait for the error message to appear and select "Cancel" + const expectedMessage = InstanceTypeInsufficientMemoryMessage('my-space', 'ml.t3.medium', 'ml.t3.large') + await getTestWindow().waitForMessage(new RegExp(expectedMessage.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))) + getTestWindow().getFirstMessage().selectItem('Cancel') + + await assert.rejects(promise, (err: ToolkitError) => err.message === 'InstanceType has insufficient memory.') }) }) diff --git a/packages/core/src/test/shared/credentials/credentialsStore.test.ts b/packages/core/src/test/shared/credentials/credentialsStore.test.ts index 4182de87250..1b85d785161 100644 --- a/packages/core/src/test/shared/credentials/credentialsStore.test.ts +++ b/packages/core/src/test/shared/credentials/credentialsStore.test.ts @@ -39,6 +39,7 @@ describe('CredentialsStore', async function () { return { getCredentials: async () => testCredentials, getHashCode: () => credentialsHashCode, + getEndpointUrl: () => undefined, } as unknown as CredentialsProvider } diff --git a/packages/core/src/test/shared/credentials/loginManager.test.ts b/packages/core/src/test/shared/credentials/loginManager.test.ts index 5e2954f6942..b7bac1fd054 100644 --- a/packages/core/src/test/shared/credentials/loginManager.test.ts +++ b/packages/core/src/test/shared/credentials/loginManager.test.ts @@ -12,7 +12,13 @@ import { CredentialsProviderManager } from '../../../auth/providers/credentialsP import { AwsContext } from '../../../shared/awsContext' import { CredentialsStore } from '../../../auth/credentials/store' import { assertTelemetryCurried } from '../../testUtil' -import { DefaultStsClient } from '../../../shared/clients/stsClient' +import { + DefaultStsClient, + GetCallerIdentityResponse, + GetCallerIdentityResponseWithHeaders, +} from '../../../shared/clients/stsClient' +import globals from '../../../shared/extensionGlobals' +import { localStackConnectionHeader, localStackConnectionString } from '../../../auth/utils' describe('LoginManager', async function () { let sandbox: sinon.SinonSandbox @@ -104,17 +110,21 @@ describe('LoginManager', async function () { assertTelemetry({ result: 'Succeeded', passive, credentialType, credentialSourceId }) }) - it('logs out if credentials could not be retrieved', async function () { - const passive = true - getCredentialsProviderStub.reset() - getCredentialsProviderStub.resolves(undefined) + // Helper function to avoid duplicating code + async function assertUndefinedCredentialsOnLogin(passive: boolean, sampleCredentialsId: CredentialsId) { const setCredentialsStub = sandbox.stub(awsContext, 'setCredentials').callsFake(async (credentials) => { // Verify that logout is called assert.strictEqual(credentials, undefined) }) - await loginManager.login({ passive, providerId: sampleCredentialsId }) assert.strictEqual(setCredentialsStub.callCount, 1, 'Expected awsContext setCredentials to be called once') + } + + it('logs out if credentials could not be retrieved', async function () { + const passive = true + getCredentialsProviderStub.reset() + getCredentialsProviderStub.resolves(undefined) + await assertUndefinedCredentialsOnLogin(passive, sampleCredentialsId) assertTelemetry({ result: 'Failed', passive }) }) @@ -122,13 +132,7 @@ describe('LoginManager', async function () { const passive = false getAccountIdStub.reset() getAccountIdStub.resolves(undefined) - const setCredentialsStub = sandbox.stub(awsContext, 'setCredentials').callsFake(async (credentials) => { - // Verify that logout is called - assert.strictEqual(credentials, undefined) - }) - - await loginManager.login({ passive, providerId: sampleCredentialsId }) - assert.strictEqual(setCredentialsStub.callCount, 1, 'Expected awsContext setCredentials to be called once') + await assertUndefinedCredentialsOnLogin(passive, sampleCredentialsId) assertTelemetry({ result: 'Failed', passive, credentialType, credentialSourceId }) }) @@ -136,13 +140,132 @@ describe('LoginManager', async function () { const passive = false getAccountIdStub.reset() getAccountIdStub.throws('Simulating getAccountId throwing an Error') - const setCredentialsStub = sandbox.stub(awsContext, 'setCredentials').callsFake(async (credentials) => { - // Verify that logout is called - assert.strictEqual(credentials, undefined) + await assertUndefinedCredentialsOnLogin(passive, sampleCredentialsId) + assertTelemetry({ result: 'Failed', passive, credentialType, credentialSourceId }) + }) + + describe('validateCredentials', function () { + let globalStateUpdateStub: sinon.SinonStub + + beforeEach(function () { + globalStateUpdateStub = sandbox.stub(globals.globalState, 'update') }) - await loginManager.login({ passive, providerId: sampleCredentialsId }) - assert.strictEqual(setCredentialsStub.callCount, 1, 'Expected awsContext setCredentials to be called once') - assertTelemetry({ result: 'Failed', passive, credentialType, credentialSourceId }) + it('validates credentials successfully and returns account ID', async function () { + const mockCallerIdentity: GetCallerIdentityResponse = { + Account: 'AccountId1234', + Arn: 'arn:aws:iam::AccountId1234:user/test-user', + UserId: 'AIDACKCEXAMPLEEXAMPLE', + } + getAccountIdStub.reset() + getAccountIdStub.resolves(mockCallerIdentity) + + const result = await loginManager.validateCredentials(sampleCredentials) + + assert.strictEqual(result, 'AccountId1234') + assert.strictEqual(getAccountIdStub.callCount, 1) + assert.strictEqual(globalStateUpdateStub.callCount, 1) + assert.strictEqual(globalStateUpdateStub.firstCall.args[0], 'aws.toolkit.externalConnection') + assert.strictEqual(globalStateUpdateStub.firstCall.args[1], undefined) + }) + + it('validates credentials with custom endpoint URL', async function () { + const customEndpoint = 'https://custom-endpoint.example.com' + const mockCallerIdentity: GetCallerIdentityResponse = { + Account: 'AccountId1234', + } + getAccountIdStub.reset() + getAccountIdStub.resolves(mockCallerIdentity) + + const result = await loginManager.validateCredentials(sampleCredentials, customEndpoint) + + assert.strictEqual(result, 'AccountId1234') + assert.strictEqual(getAccountIdStub.callCount, 1) + }) + + it('throws error when account ID is missing', async function () { + const mockCallerIdentity: GetCallerIdentityResponse = { + Arn: 'arn:aws:iam::AccountId1234:user/test-user', + UserId: 'AIDACKCEXAMPLEEXAMPLE', + } + getAccountIdStub.reset() + getAccountIdStub.resolves(mockCallerIdentity) + + await assert.rejects(async () => await loginManager.validateCredentials(sampleCredentials), { + message: 'Could not determine Account Id for credentials', + }) + }) + + it('propagates STS client errors', async function () { + const testError = new Error('STS service unavailable') + getAccountIdStub.reset() + getAccountIdStub.rejects(testError) + + await assert.rejects(async () => await loginManager.validateCredentials(sampleCredentials), testError) + }) + }) + + describe('detectExternalConnection', function () { + let globalStateUpdateStub: sinon.SinonStub + + beforeEach(function () { + globalStateUpdateStub = sandbox.stub(globals.globalState, 'update') + }) + + it('detects LocalStack connection and updates global state', async function () { + const mockCallerIdentityWithLocalStack: GetCallerIdentityResponseWithHeaders = { + Account: 'AccountId1234', + Arn: 'arn:aws:iam::AccountId1234:user/test-user', + UserId: 'AIDACKCEXAMPLEEXAMPLE', + $httpHeaders: { + [localStackConnectionHeader]: 'true', + 'content-type': 'application/json', + }, + } + getAccountIdStub.reset() + getAccountIdStub.resolves(mockCallerIdentityWithLocalStack) + + await loginManager.validateCredentials(sampleCredentials) + + assert.strictEqual(globalStateUpdateStub.callCount, 1) + assert.strictEqual(globalStateUpdateStub.firstCall.args[0], 'aws.toolkit.externalConnection') + assert.strictEqual(globalStateUpdateStub.firstCall.args[1], localStackConnectionString) + }) + + it('does not detect external connection when LocalStack header is missing', async function () { + const mockCallerIdentityWithoutLocalStack: GetCallerIdentityResponseWithHeaders = { + Account: 'AccountId1234', + Arn: 'arn:aws:iam::AccountId1234:user/test-user', + UserId: 'AIDACKCEXAMPLEEXAMPLE', + $httpHeaders: { + 'content-type': 'application/json', + 'x-amzn-requestid': 'test-request-id', + }, + } + getAccountIdStub.reset() + getAccountIdStub.resolves(mockCallerIdentityWithoutLocalStack) + + await loginManager.validateCredentials(sampleCredentials) + + assert.strictEqual(globalStateUpdateStub.callCount, 1) + assert.strictEqual(globalStateUpdateStub.firstCall.args[0], 'aws.toolkit.externalConnection') + assert.strictEqual(globalStateUpdateStub.firstCall.args[1], undefined) + }) + + it('handles response with no $httpHeaders property', async function () { + const mockCallerIdentityWithoutHeaders: GetCallerIdentityResponse = { + Account: 'AccountId1234', + Arn: 'arn:aws:iam::AccountId1234:user/test-user', + UserId: 'AIDACKCEXAMPLEEXAMPLE', + } + getAccountIdStub.reset() + getAccountIdStub.resolves(mockCallerIdentityWithoutHeaders) + + await loginManager.validateCredentials(sampleCredentials) + + assert.strictEqual(globalStateUpdateStub.callCount, 1) + assert.strictEqual(globalStateUpdateStub.firstCall.args[0], 'aws.toolkit.externalConnection') + assert.strictEqual(globalStateUpdateStub.firstCall.args[1], undefined) + }) }) }) diff --git a/packages/core/src/test/shared/defaultAwsContext.test.ts b/packages/core/src/test/shared/defaultAwsContext.test.ts index ad15e0ee1ce..6623fa5dee7 100644 --- a/packages/core/src/test/shared/defaultAwsContext.test.ts +++ b/packages/core/src/test/shared/defaultAwsContext.test.ts @@ -4,7 +4,7 @@ */ import assert from 'assert' -import * as AWS from 'aws-sdk' +import { AwsCredentialIdentity } from '@aws-sdk/types' import { AwsContextCredentials } from '../../shared/awsContext' import { DefaultAwsContext } from '../../shared/awsContext' @@ -83,11 +83,52 @@ describe('DefaultAwsContext', function () { }) }) - function makeSampleAwsContextCredentials(): AwsContextCredentials { + it('gets endpoint URL from credentials', async function () { + const testEndpointUrl = 'https://custom-endpoint.example.com' + const awsCredentials = makeSampleAwsContextCredentials(testEndpointUrl) + + const testContext = new DefaultAwsContext() + + await testContext.setCredentials(awsCredentials) + assert.strictEqual(testContext.getCredentialEndpointUrl(), testEndpointUrl) + }) + + it('returns undefined endpoint URL when not set in credentials', async function () { + const awsCredentials = makeSampleAwsContextCredentials() + + const testContext = new DefaultAwsContext() + + await testContext.setCredentials(awsCredentials) + assert.strictEqual(testContext.getCredentialEndpointUrl(), undefined) + }) + + it('returns undefined endpoint URL when no credentials are set', async function () { + const testContext = new DefaultAwsContext() + + assert.strictEqual(testContext.getCredentialEndpointUrl(), undefined) + }) + + it('returns undefined endpoint URL after setting undefined credentials', async function () { + const testEndpointUrl = 'https://custom-endpoint.example.com' + const awsCredentials = makeSampleAwsContextCredentials(testEndpointUrl) + + const testContext = new DefaultAwsContext() + + // First set credentials with endpoint URL + await testContext.setCredentials(awsCredentials) + assert.strictEqual(testContext.getCredentialEndpointUrl(), testEndpointUrl) + + // Then clear credentials + await testContext.setCredentials(undefined) + assert.strictEqual(testContext.getCredentialEndpointUrl(), undefined) + }) + + function makeSampleAwsContextCredentials(endpointUrl?: string): AwsContextCredentials { return { - credentials: {} as any as AWS.Credentials, + credentials: {} as AwsCredentialIdentity, credentialsId: 'qwerty', accountId: testAccountIdValue, + endpointUrl, } } }) diff --git a/packages/core/src/test/shared/errors.test.ts b/packages/core/src/test/shared/errors.test.ts index 32d18186912..093feceb012 100644 --- a/packages/core/src/test/shared/errors.test.ts +++ b/packages/core/src/test/shared/errors.test.ts @@ -683,6 +683,49 @@ describe('util', function () { assert.deepStrictEqual(scrubNames('unix ~jdoe123/.aws/config failed', fakeUser), 'unix ~x/.aws/config failed') assert.deepStrictEqual(scrubNames('unix ../../.aws/config failed', fakeUser), 'unix ../../.aws/config failed') assert.deepStrictEqual(scrubNames('unix ~/.aws/config failed', fakeUser), 'unix ~/.aws/config failed') + + // Profile name scrubbing - tests all three patterns + + // Pattern 1: profile name with space separator + const profileTest1 = scrubNames('Error with profile my-profile', fakeUser) + assert.deepStrictEqual(profileTest1, 'Error with profile [REDACTED]', 'Should handle space-separated profile') + assert.ok(!profileTest1.includes('my-profile'), 'Original profile name should not appear') + + // Pattern 2: profile name with single quotes + const profileTest2 = scrubNames("Failed to load profile 'production-admin'", fakeUser) + assert.deepStrictEqual(profileTest2, 'Failed to load profile [REDACTED]', 'Should handle single-quoted profile') + assert.ok(!profileTest2.includes('production-admin'), 'Original profile name should not appear') + assert.ok(!profileTest2.includes("'"), 'Closing quote should be removed') + + // Pattern 2: profile name with double quotes + const profileTest3 = scrubNames('Using profile "staging-env" for authentication', fakeUser) + assert.deepStrictEqual( + profileTest3, + 'Using profile [REDACTED] for authentication', + 'Should handle double-quoted profile' + ) + assert.ok(!profileTest3.includes('staging-env'), 'Original profile name should not appear') + assert.ok(!profileTest3.includes('"'), 'Closing quote should be removed') + + // Pattern 3: profile name with colon separator + const profileTest4 = scrubNames('Profile: dev-account not found', fakeUser) + assert.deepStrictEqual(profileTest4, 'Profile [REDACTED] not found', 'Should handle colon-separated profile') + assert.ok(!profileTest4.includes('dev-account'), 'Original profile name should not appear') + + // Case preservation tests + const profileTest5 = scrubNames('PROFILE: admin-user failed', fakeUser) + assert.deepStrictEqual(profileTest5, 'PROFILE [REDACTED] failed', 'Should preserve uppercase PROFILE') + + const profileTest6 = scrubNames("Profile 'test-123' is invalid", fakeUser) + assert.deepStrictEqual(profileTest6, 'Profile [REDACTED] is invalid', 'Should preserve capitalized Profile') + + // Multiple profiles in one message + const profileTest7 = scrubNames("Switching from profile 'old-profile' to profile 'new-profile'", fakeUser) + assert.ok( + !profileTest7.includes('old-profile') && !profileTest7.includes('new-profile'), + 'Should redact multiple profiles' + ) + assert.ok(profileTest7.includes('[REDACTED]'), 'Should contain redaction markers') }) }) diff --git a/packages/core/src/test/shared/extensionUtilities.test.ts b/packages/core/src/test/shared/extensionUtilities.test.ts index 16d3792c63b..ce4c764d3d5 100644 --- a/packages/core/src/test/shared/extensionUtilities.test.ts +++ b/packages/core/src/test/shared/extensionUtilities.test.ts @@ -5,7 +5,7 @@ import assert from 'assert' -import { AWSError } from 'aws-sdk' +import { ServiceException } from '@smithy/smithy-client' import * as sinon from 'sinon' import { DefaultEc2MetadataClient } from '../../shared/clients/ec2MetadataClient' import * as vscode from 'vscode' @@ -98,14 +98,14 @@ describe('initializeComputeRegion, getComputeRegion', async function () { }) it('returns "unknown" if cloud9 and the MetadataService request fails', async function () { - sandbox.stub(metadataService, 'getInstanceIdentity').rejects({} as AWSError) + sandbox.stub(metadataService, 'getInstanceIdentity').rejects({} as ServiceException) await initializeComputeRegion(metadataService, true) assert.strictEqual(getComputeRegion(), 'unknown') }) it('returns "unknown" if sagemaker and the MetadataService request fails', async function () { - sandbox.stub(metadataService, 'getInstanceIdentity').rejects({} as AWSError) + sandbox.stub(metadataService, 'getInstanceIdentity').rejects({} as ServiceException) await initializeComputeRegion(metadataService, false, true) assert.strictEqual(getComputeRegion(), 'unknown') diff --git a/packages/core/src/test/shared/extensions/ssh.test.ts b/packages/core/src/test/shared/extensions/ssh.test.ts index e7a012f182d..4e9bda41217 100644 --- a/packages/core/src/test/shared/extensions/ssh.test.ts +++ b/packages/core/src/test/shared/extensions/ssh.test.ts @@ -9,7 +9,7 @@ import { createBoundProcess } from '../../../shared/remoteSession' import { createExecutableFile, createTestWorkspaceFolder } from '../../testUtil' import { WorkspaceFolder } from 'vscode' import path from 'path' -import { SSM } from 'aws-sdk' +import { StartSessionResponse } from '@aws-sdk/client-ssm' import { fs } from '../../../shared/fs/fs' import { isWin } from '../../../shared/vscode/env' @@ -74,7 +74,7 @@ describe('testSshConnection', function () { SessionId: 'testSession', StreamUrl: 'testUrl', TokenValue: 'testToken', - } as SSM.StartSessionResponse + } as StartSessionResponse await createExecutableFile(sshPath, echoEnvVarsCmd(['MY_VAR'])) const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', session) @@ -86,12 +86,12 @@ describe('testSshConnection', function () { SessionId: 'testSession1', StreamUrl: 'testUrl1', TokenValue: 'testToken1', - } as SSM.StartSessionResponse + } as StartSessionResponse const newSession = { SessionId: 'testSession2', StreamUrl: 'testUrl2', TokenValue: 'testToken2', - } as SSM.StartSessionResponse + } as StartSessionResponse const envProvider = async () => ({ SESSION_ID: oldSession.SessionId, STREAM_URL: oldSession.StreamUrl, @@ -108,7 +108,7 @@ describe('testSshConnection', function () { const executableFileContent = isWin() ? `echo "%1 %2"` : `echo "$1 $2"` const process = createBoundProcess(async () => ({})) await createExecutableFile(sshPath, executableFileContent) - const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', {} as SSM.StartSessionResponse) + const r = await testSshConnection(process, 'localhost', sshPath, 'test-user', {} as StartSessionResponse) assertOutputContains(r.stdout, '-T') assertOutputContains(r.stdout, 'test-user@localhost') }) diff --git a/packages/core/src/test/shared/sam/build.test.ts b/packages/core/src/test/shared/sam/build.test.ts index 8043696d772..bed4fee7e25 100644 --- a/packages/core/src/test/shared/sam/build.test.ts +++ b/packages/core/src/test/shared/sam/build.test.ts @@ -512,6 +512,9 @@ describe('SAM runBuild', () => { }) .build() + // Reset the spy before running the test to ensure clean state + spyRunInterminal.resetHistory() + // Instead of await runBuild(), prefer this to avoid flakiness due to race condition await delayedRunBuild() diff --git a/packages/core/src/test/shared/sam/cli/samCliFeatureRegistry.test.ts b/packages/core/src/test/shared/sam/cli/samCliFeatureRegistry.test.ts new file mode 100644 index 00000000000..7efe8aff70a --- /dev/null +++ b/packages/core/src/test/shared/sam/cli/samCliFeatureRegistry.test.ts @@ -0,0 +1,179 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { detectFeaturesInTemplate } from '../../../../shared/sam/cli/samCliFeatureRegistry' +import * as samUtils from '../../../../shared/sam/utils' + +describe('samCliFeatureRegistry', () => { + let sandbox: sinon.SinonSandbox + let getSamCliPathAndVersionStub: sinon.SinonStub + + beforeEach(() => { + sandbox = sinon.createSandbox() + getSamCliPathAndVersionStub = sandbox.stub(samUtils, 'getSamCliPathAndVersion') + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('detectFeaturesInTemplate', () => { + const testCases = [ + { + name: 'should detect Capacity Provider resource type when version is unsupported', + samVersion: '1.148.0', + template: { + Resources: { + MyCapacityProvider: { + Type: 'AWS::Serverless::CapacityProvider', + Properties: {}, + }, + }, + }, + expectedCount: 1, + expectedIds: ['CAPACITY_PROVIDER'], + }, + { + name: 'should return empty array when SAM CLI version meets all requirements', + samVersion: '1.149.0', + template: { + Resources: { + MyCapacityProvider: { + Type: 'AWS::Serverless::CapacityProvider', + Properties: {}, + }, + }, + }, + expectedCount: 0, + expectedIds: [], + }, + { + name: 'should return empty array when SAM CLI version exceeds all requirements', + samVersion: '2.0.0', + template: { + Resources: { + MyCapacityProvider: { + Type: 'AWS::Serverless::CapacityProvider', + Properties: {}, + }, + }, + }, + expectedCount: 0, + expectedIds: [], + }, + { + name: 'should detect CapacityProviderConfig property on Function', + samVersion: '1.148.0', + template: { + Resources: { + MyFunction: { + Type: 'AWS::Serverless::Function', + Properties: { + CapacityProviderConfig: 'MyCapacityProvider', + }, + }, + }, + }, + expectedCount: 1, + expectedIds: ['CAPACITY_PROVIDER_CONFIG'], + }, + { + name: 'should detect CapacityProviderConfig in Globals', + samVersion: '1.148.0', + template: { + Globals: { + Function: { + CapacityProviderConfig: 'MyCapacityProvider', + }, + }, + Resources: {}, + }, + expectedCount: 1, + expectedIds: ['CAPACITY_PROVIDER_CONFIG'], + }, + { + name: 'should detect multiple features', + samVersion: '1.148.0', + template: { + Resources: { + MyCapacityProvider: { + Type: 'AWS::Serverless::CapacityProvider', + Properties: {}, + }, + MyFunction: { + Type: 'AWS::Serverless::Function', + Properties: { + CapacityProviderConfig: 'MyCapacityProvider', + }, + }, + }, + }, + expectedCount: 2, + expectedIds: ['CAPACITY_PROVIDER', 'CAPACITY_PROVIDER_CONFIG'], + }, + { + name: 'should not detect duplicate features', + samVersion: '1.148.0', + template: { + Resources: { + MyFunction1: { + Type: 'AWS::Serverless::Function', + Properties: { + CapacityProviderConfig: 'MyCapacityProvider', + }, + }, + MyFunction2: { + Type: 'AWS::Serverless::Function', + Properties: { + CapacityProviderConfig: 'MyCapacityProvider', + }, + }, + }, + }, + expectedCount: 1, + expectedIds: ['CAPACITY_PROVIDER_CONFIG'], + }, + { + name: 'should return empty array for template without special features', + samVersion: '1.148.0', + template: { + Resources: { + MyFunction: { + Type: 'AWS::Serverless::Function', + Properties: { + Runtime: 'nodejs18.x', + }, + }, + }, + }, + expectedCount: 0, + expectedIds: [], + }, + { + name: 'should handle empty template', + samVersion: '1.148.0', + template: {}, + expectedCount: 0, + expectedIds: [], + }, + ] + + for (const { name, samVersion, template, expectedCount, expectedIds } of testCases) { + it(name, async () => { + getSamCliPathAndVersionStub.resolves({ path: '/path/to/sam', parsedVersion: samVersion }) + + const { unsupported, version } = await detectFeaturesInTemplate(template) + + assert.strictEqual(version, samVersion) + assert.strictEqual(unsupported.length, expectedCount) + for (const id of expectedIds) { + assert(unsupported.some((f) => f.id === id)) + } + }) + } + }) +}) diff --git a/packages/core/src/test/shared/sam/cli/samCliListResources.test.ts b/packages/core/src/test/shared/sam/cli/samCliListResources.test.ts index e03500dd2bf..51efce4af34 100644 --- a/packages/core/src/test/shared/sam/cli/samCliListResources.test.ts +++ b/packages/core/src/test/shared/sam/cli/samCliListResources.test.ts @@ -4,12 +4,17 @@ */ import assert from 'assert' +import * as sinon from 'sinon' import { runSamCliListResource, SamCliListResourcesParameters } from '../../../../shared/sam/cli/samCliListResources' import { assertArgIsPresent, assertArgsContainArgument, MockSamCliProcessInvoker } from './samCliTestUtils' import { getTestLogger } from '../../../globalSetup.test' +import * as featureRegistry from '../../../../shared/sam/cli/samCliFeatureRegistry' describe('runSamCliListResource', function () { let invokeCount: number + let sandbox: sinon.SinonSandbox + let validateSamCliVersionForTemplateFileStub: sinon.SinonStub + let showWarningStub: sinon.SinonStub const fakeTemplateFile = 'template.yaml' const fakeStackName = 'testStack' const fakeRegion = 'us-west-2' @@ -17,6 +22,15 @@ describe('runSamCliListResource', function () { beforeEach(function () { invokeCount = 0 + sandbox = sinon.createSandbox() + validateSamCliVersionForTemplateFileStub = sandbox + .stub(featureRegistry, 'validateSamCliVersionForTemplateFile') + .resolves() + showWarningStub = sandbox.stub(featureRegistry, 'showWarningWithSamCliUpdateInstruction').resolves() + }) + + afterEach(function () { + sandbox.restore() }) function makeSampleParameters(region?: string): SamCliListResourcesParameters { @@ -76,4 +90,42 @@ describe('runSamCliListResource', function () { const logs = logger.getLoggedEntries() assert.ok(logs.find((entry) => entry === message)) }) + + it('validates template before invoking SAM CLI', async function () { + const invoker = new MockSamCliProcessInvoker(() => {}) + + await runSamCliListResource(makeSampleParameters(), invoker) + + assert.ok( + validateSamCliVersionForTemplateFileStub.calledOnce, + 'validateSamCliVersionForTemplateFile should be called once' + ) + }) + + it('returns empty array when validation fails', async function () { + const validationError = new Error('SAM CLI version too old') + validateSamCliVersionForTemplateFileStub.rejects(validationError) + const invoker = new MockSamCliProcessInvoker(() => { + invokeCount++ + }) + + const result = await runSamCliListResource(makeSampleParameters(), invoker) + + assert.strictEqual(invokeCount, 0, 'SAM CLI should not be invoked when validation fails') + assert.deepStrictEqual(result, [], 'Should return empty array on validation failure') + assert.ok(showWarningStub.calledOnce, 'Should show warning message') + }) + + it('shows user-friendly error message when validation fails', async function () { + const validationError = new Error('Your SAM CLI version does not support feature X') + validateSamCliVersionForTemplateFileStub.rejects(validationError) + const invoker = new MockSamCliProcessInvoker(() => {}) + + await runSamCliListResource(makeSampleParameters(), invoker) + + assert.ok(showWarningStub.calledOnce) + const errorMessage = showWarningStub.firstCall.args[0] + assert.ok(errorMessage.includes('Failed to run SAM CLI list resources')) + assert.ok(errorMessage.includes(validationError.message)) + }) }) diff --git a/packages/core/src/test/shared/sam/debugger/samDebugConfigProvider.test.ts b/packages/core/src/test/shared/sam/debugger/samDebugConfigProvider.test.ts index 9fe7c76e842..d00273f6426 100644 --- a/packages/core/src/test/shared/sam/debugger/samDebugConfigProvider.test.ts +++ b/packages/core/src/test/shared/sam/debugger/samDebugConfigProvider.test.ts @@ -47,6 +47,7 @@ import { CredentialsProvider } from '../../../../auth/providers/credentials' import globals from '../../../../shared/extensionGlobals' import { isCI } from '../../../../shared/vscode/env' import { fs } from '../../../../shared' +import { Runtime } from '@aws-sdk/client-lambda' /** * Asserts the contents of a "launch config" (the result of `makeConfig()` or @@ -317,7 +318,12 @@ describe('SamDebugConfigurationProvider', async function () { ) // No workspace folder: + // Stub vscode.workspace.workspaceFolders to be undefined to ensure rejection + sandbox.stub(vscode.workspace, 'workspaceFolders').value(undefined) await assert.rejects(() => debugConfigProvider.makeConfig(undefined, config.config)) + // Restore for subsequent tests + sandbox.restore() + sandbox = sinon.createSandbox() // No launch.json (vscode will pass an empty config.request): await assert.rejects(() => debugConfigProvider.makeConfig(undefined, { ...config.config, request: '' })) @@ -2085,9 +2091,9 @@ describe('SamDebugConfigurationProvider', async function () { assertEqualLaunchConfigs(actualNoDebug, expectedNoDebug) }) - it('target=code: python 3.7', async function () { + it('target=code: python 3.14', async function () { const appDir = pathutil.normalize( - path.join(testutil.getProjectDir(), 'testFixtures/workspaceFolder/python3.7-plain-sam-app') + path.join(testutil.getProjectDir(), 'testFixtures/workspaceFolder/python3.14-plain-sam-app') ) const relPayloadPath = `events/event.json` const absPayloadPath = `${appDir}/${relPayloadPath}` @@ -2102,7 +2108,7 @@ describe('SamDebugConfigurationProvider', async function () { projectRoot: 'hello_world', }, lambda: { - runtime: 'python3.7', + runtime: 'python3.14', payload: { path: relPayloadPath, }, @@ -2115,7 +2121,7 @@ describe('SamDebugConfigurationProvider', async function () { const expected: SamLaunchRequestArgs = { awsCredentials: fakeCredentials, request: 'attach', // Input "direct-invoke", output "attach". - runtime: 'python3.7', + runtime: 'python3.14' as Runtime, runtimeFamily: lambdaModel.RuntimeFamily.Python, type: AWS_SAM_DEBUG_TYPE, handlerName: 'app.lambda_handler', @@ -2182,7 +2188,7 @@ describe('SamDebugConfigurationProvider', async function () { Handler: ${expected.handlerName} CodeUri: >- ${expected.codeRoot} - Runtime: python3.7 + Runtime: python3.14 ` ) @@ -2238,21 +2244,21 @@ describe('SamDebugConfigurationProvider', async function () { assertEqualLaunchConfigs(actualNoDebug, expectedNoDebug) }) - it('target=template: python 3.7 (deep project tree)', async function () { + it('target=template: python 3.14 (deep project tree)', async function () { // To test a deeper tree, use "testFixtures/workspaceFolder/" as the root. const appDir = pathutil.normalize(path.join(testutil.getProjectDir(), 'testFixtures/workspaceFolder/')) const folder = testutil.getWorkspaceFolder(appDir) const input = { type: AWS_SAM_DEBUG_TYPE, - name: 'test-py37-template', + name: 'test-py314-template', request: DIRECT_INVOKE_TYPE, invokeTarget: { target: TEMPLATE_TARGET_TYPE, - templatePath: 'python3.7-plain-sam-app/template.yaml', + templatePath: 'python3.14-plain-sam-app/template.yaml', logicalId: 'HelloWorldFunction', }, } - const templatePath = vscode.Uri.file(path.join(appDir, 'python3.7-plain-sam-app/template.yaml')) + const templatePath = vscode.Uri.file(path.join(appDir, 'python3.14-plain-sam-app/template.yaml')) // Invoke with noDebug=false (the default). const actual = (await debugConfigProvider.makeConfig(folder, input))! @@ -2260,7 +2266,7 @@ describe('SamDebugConfigurationProvider', async function () { const expected: SamLaunchRequestArgs = { awsCredentials: fakeCredentials, request: 'attach', // Input "direct-invoke", output "attach". - runtime: 'python3.7', + runtime: 'python3.14' as Runtime, runtimeFamily: lambdaModel.RuntimeFamily.Python, type: AWS_SAM_DEBUG_TYPE, handlerName: 'app.lambda_handler', @@ -2272,7 +2278,7 @@ describe('SamDebugConfigurationProvider', async function () { baseBuildDir: actual.baseBuildDir, // Random, sanity-checked by assertEqualLaunchConfigs(). envFile: undefined, eventPayloadFile: undefined, - codeRoot: pathutil.normalize(path.join(appDir, 'python3.7-plain-sam-app/hello_world')), + codeRoot: pathutil.normalize(path.join(appDir, 'python3.14-plain-sam-app/hello_world')), debugArgs: [ `/tmp/lambci_debug_files/py_debug_wrapper.py --listen 0.0.0.0:${actual.debugPort} --wait-for-client --log-to-stderr --debug`, ], @@ -2299,7 +2305,7 @@ describe('SamDebugConfigurationProvider', async function () { host: 'localhost', pathMappings: [ { - localRoot: pathutil.normalize(path.join(appDir, 'python3.7-plain-sam-app/hello_world')), + localRoot: pathutil.normalize(path.join(appDir, 'python3.14-plain-sam-app/hello_world')), remoteRoot: '/var/task', }, ], @@ -2354,18 +2360,18 @@ describe('SamDebugConfigurationProvider', async function () { await assertEqualNoDebugTemplateTarget(input, expected, folder, debugConfigProvider, true) }) - it('target=api: python 3.7 (deep project tree)', async function () { + it('target=api: python 3.14 (deep project tree)', async function () { // Use "testFixtures/workspaceFolder/" as the project root to test // a deeper tree. const appDir = pathutil.normalize(path.join(testutil.getProjectDir(), 'testFixtures/workspaceFolder/')) const folder = testutil.getWorkspaceFolder(appDir) const input: AwsSamDebuggerConfiguration = { type: AWS_SAM_DEBUG_TYPE, - name: 'test-py37-api', + name: 'test-py314-api', request: DIRECT_INVOKE_TYPE, invokeTarget: { target: API_TARGET_TYPE, - templatePath: 'python3.7-plain-sam-app/template.yaml', + templatePath: 'python3.14-plain-sam-app/template.yaml', logicalId: 'HelloWorldFunction', }, api: { @@ -2377,7 +2383,7 @@ describe('SamDebugConfigurationProvider', async function () { querystring: 'name1=value1&foo&bar', }, } - const templatePath = vscode.Uri.file(path.join(appDir, 'python3.7-plain-sam-app/template.yaml')) + const templatePath = vscode.Uri.file(path.join(appDir, 'python3.14-plain-sam-app/template.yaml')) // Invoke with noDebug=false (the default). const actual = (await debugConfigProvider.makeConfig(folder, input))! @@ -2385,7 +2391,7 @@ describe('SamDebugConfigurationProvider', async function () { const expected: SamLaunchRequestArgs = { awsCredentials: fakeCredentials, request: 'attach', // Input "direct-invoke", output "attach". - runtime: 'python3.7', + runtime: 'python3.14' as Runtime, runtimeFamily: lambdaModel.RuntimeFamily.Python, type: AWS_SAM_DEBUG_TYPE, handlerName: 'app.lambda_handler', @@ -2397,7 +2403,7 @@ describe('SamDebugConfigurationProvider', async function () { baseBuildDir: actual.baseBuildDir, // Random, sanity-checked by assertEqualLaunchConfigs(). envFile: undefined, eventPayloadFile: undefined, - codeRoot: pathutil.normalize(path.join(appDir, 'python3.7-plain-sam-app/hello_world')), + codeRoot: pathutil.normalize(path.join(appDir, 'python3.14-plain-sam-app/hello_world')), debugArgs: [ `/tmp/lambci_debug_files/py_debug_wrapper.py --listen 0.0.0.0:${actual.debugPort} --wait-for-client --log-to-stderr --debug`, ], @@ -2426,7 +2432,7 @@ describe('SamDebugConfigurationProvider', async function () { host: 'localhost', pathMappings: [ { - localRoot: pathutil.normalize(path.join(appDir, 'python3.7-plain-sam-app/hello_world')), + localRoot: pathutil.normalize(path.join(appDir, 'python3.14-plain-sam-app/hello_world')), remoteRoot: '/var/task', }, ], @@ -2450,25 +2456,25 @@ describe('SamDebugConfigurationProvider', async function () { await assertEqualNoDebugTemplateTarget(input, expected, folder, debugConfigProvider, true) }) - it('target=template: Image python 3.7', async function () { + it('target=template: Image python 3.14', async function () { // Use "testFixtures/workspaceFolder/" as the project root to test // a deeper tree. const appDir = pathutil.normalize(path.join(testutil.getProjectDir(), 'testFixtures/workspaceFolder/')) const folder = testutil.getWorkspaceFolder(appDir) const input = { type: AWS_SAM_DEBUG_TYPE, - name: 'test-py37-image-template', + name: 'test-py314-image-template', request: DIRECT_INVOKE_TYPE, invokeTarget: { target: TEMPLATE_TARGET_TYPE, - templatePath: 'python3.7-image-sam-app/template.yaml', + templatePath: 'python3.14-image-sam-app/template.yaml', logicalId: 'HelloWorldFunction', }, lambda: { - runtime: 'python3.7', + runtime: 'python3.14', }, } - const templatePath = vscode.Uri.file(path.join(appDir, 'python3.7-image-sam-app/template.yaml')) + const templatePath = vscode.Uri.file(path.join(appDir, 'python3.14-image-sam-app/template.yaml')) // Invoke with noDebug=false (the default). const actual = (await debugConfigProvider.makeConfig(folder, input))! @@ -2476,7 +2482,7 @@ describe('SamDebugConfigurationProvider', async function () { const expected: SamLaunchRequestArgs = { awsCredentials: fakeCredentials, request: 'attach', // Input "direct-invoke", output "attach". - runtime: 'python3.7', + runtime: 'python3.14' as Runtime, runtimeFamily: lambdaModel.RuntimeFamily.Python, type: AWS_SAM_DEBUG_TYPE, handlerName: 'HelloWorldFunction', @@ -2488,9 +2494,9 @@ describe('SamDebugConfigurationProvider', async function () { baseBuildDir: actual.baseBuildDir, // Random, sanity-checked by assertEqualLaunchConfigs(). envFile: undefined, eventPayloadFile: undefined, - codeRoot: pathutil.normalize(path.join(appDir, 'python3.7-image-sam-app/hello_world')), + codeRoot: pathutil.normalize(path.join(appDir, 'python3.14-image-sam-app/hello_world')), debugArgs: [ - `/var/lang/bin/python3.7 /tmp/lambci_debug_files/py_debug_wrapper.py --listen 0.0.0.0:${actual.debugPort} --wait-for-client --log-to-stderr /var/runtime/bootstrap --debug`, + `/var/lang/bin/python3.14 /tmp/lambci_debug_files/py_debug_wrapper.py --listen 0.0.0.0:${actual.debugPort} --wait-for-client --log-to-stderr /var/runtime/bootstrap.py --debug`, ], apiPort: undefined, debugPort: actual.debugPort, @@ -2500,7 +2506,7 @@ describe('SamDebugConfigurationProvider', async function () { environmentVariables: {}, memoryMb: undefined, timeoutSec: 3, - runtime: 'python3.7', + runtime: 'python3.14', }, name: input.name, templatePath: pathutil.normalize(path.join(path.dirname(templatePath.fsPath), 'template.yaml')), @@ -2515,7 +2521,7 @@ describe('SamDebugConfigurationProvider', async function () { host: 'localhost', pathMappings: [ { - localRoot: pathutil.normalize(path.join(appDir, 'python3.7-image-sam-app/hello_world')), + localRoot: pathutil.normalize(path.join(appDir, 'python3.14-image-sam-app/hello_world')), remoteRoot: '/var/task', }, ], @@ -2587,7 +2593,7 @@ describe('SamDebugConfigurationProvider', async function () { it('verify python debug option not set for non-debug log level', async function () { const appDir = pathutil.normalize( - path.join(testutil.getProjectDir(), 'testFixtures/workspaceFolder/python3.7-plain-sam-app') + path.join(testutil.getProjectDir(), 'testFixtures/workspaceFolder/python3.14-plain-sam-app') ) const relPayloadPath = `events/event.json` const folder = testutil.getWorkspaceFolder(appDir) @@ -2601,7 +2607,7 @@ describe('SamDebugConfigurationProvider', async function () { projectRoot: 'hello_world', }, lambda: { - runtime: 'python3.7', // Arbitrary choice of runtime for this test + runtime: 'python3.14', // Arbitrary choice of runtime for this test payload: { path: relPayloadPath, }, @@ -2906,7 +2912,7 @@ describe('ensureRelativePaths', function () { undefined, 'testName1', '/test1/project', - lambdaModel.getDefaultRuntime(lambdaModel.RuntimeFamily.NodeJS) ?? '' + lambdaModel.getDefaultRuntime(lambdaModel.RuntimeFamily.NodeJS)! ) assert.strictEqual((codeConfig.invokeTarget as CodeTargetProperties).projectRoot, '/test1/project') ensureRelativePaths(workspace, codeConfig) diff --git a/packages/core/src/test/shared/sam/samTestUtils.ts b/packages/core/src/test/shared/sam/samTestUtils.ts index 61b67446f5b..709fc1dfc1c 100644 --- a/packages/core/src/test/shared/sam/samTestUtils.ts +++ b/packages/core/src/test/shared/sam/samTestUtils.ts @@ -115,3 +115,24 @@ Resources: Bucket: !Ref SourceBucket Events: s3:ObjectCreated:* ` + +export const capacityProviderTemplateData = ` +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Resources: + MyCapacityProvider: + Type: AWS::Serverless::CapacityProvider + Properties: + InstanceRequirements: + Architectures: + - x86_64 + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: app.handler + Runtime: python3.13 + CodeUri: ./src + CapacityProviderConfig: + Arn: !GetAtt MyCapacityProvider.Arn +` diff --git a/packages/core/src/test/shared/sam/sync.test.ts b/packages/core/src/test/shared/sam/sync.test.ts index 843b0e0bbcd..da7a8086f38 100644 --- a/packages/core/src/test/shared/sam/sync.test.ts +++ b/packages/core/src/test/shared/sam/sync.test.ts @@ -43,7 +43,7 @@ import sinon from 'sinon' import { getTestWindow } from '../vscode/window' import { S3Client } from '../../../shared/clients/s3' import { RequiredProps } from '../../../shared/utilities/tsUtils' -import S3 from 'aws-sdk/clients/s3' +import { Bucket } from '@aws-sdk/client-s3' import { CloudFormationClient } from '../../../shared/clients/cloudFormation' import { intoCollection } from '../../../shared/utilities/collectionUtils' import { SamConfig, Environment, parseConfig } from '../../../shared/sam/config' @@ -2174,7 +2174,7 @@ describe('SAM sync helper functions', () => { }) const s3BucketListSummary: Array< - RequiredProps & { + RequiredProps & { readonly region: string } > = [ diff --git a/packages/core/src/test/shared/sshConfig.test.ts b/packages/core/src/test/shared/sshConfig.test.ts index 03841644e24..87a816e9255 100644 --- a/packages/core/src/test/shared/sshConfig.test.ts +++ b/packages/core/src/test/shared/sshConfig.test.ts @@ -17,7 +17,7 @@ import { connectScriptPrefix, getCodeCatalystSsmEnv, } from '../../codecatalyst/model' -import { StartDevEnvironmentSessionRequest } from 'aws-sdk/clients/codecatalyst' +import { StartDevEnvironmentSessionRequest } from '@aws-sdk/client-codecatalyst' import { mkdir, readFile } from 'fs/promises' import fs from '../../shared/fs/fs' import { globals } from '../../shared' @@ -26,6 +26,7 @@ class MockSshConfig extends SshConfig { // State variables to track logic flow. public testIsWin: boolean = false public configSection: string = '' + public exitCodeOverride: number = 0 public async getProxyCommandWrapper(command: string): Promise> { return await this.getProxyCommand(command) @@ -51,7 +52,7 @@ class MockSshConfig extends SshConfig { protected override async checkSshOnHost(): Promise { return { - exitCode: 0, + exitCode: this.exitCodeOverride, error: undefined, stdout: this.configSection, stderr: '', @@ -95,6 +96,10 @@ describe('VscodeRemoteSshConfig', async function () { }) describe('matchSshSection', async function () { + beforeEach(function () { + config.exitCodeOverride = 0 + }) + it('returns ok with match when proxycommand is present', async function () { const testSection = `proxycommandfdsafdsafd${testProxyCommand}sa342432` const result = await config.testMatchSshSection(testSection) @@ -110,6 +115,16 @@ describe('VscodeRemoteSshConfig', async function () { const match = result.unwrap() assert.strictEqual(match, undefined) }) + + it('returns error when ssh check fails with non-zero exit code', async function () { + config.exitCodeOverride = 255 + const testSection = `some config` + const result = await config.testMatchSshSection(testSection) + assert.ok(result.isErr()) + const error = result.err() + assert.ok(error.message.includes('ssh check against host failed')) + assert.ok(error.message.includes('255')) + }) }) describe('verifySSHHost', async function () { @@ -122,6 +137,7 @@ describe('VscodeRemoteSshConfig', async function () { }) beforeEach(function () { + config.exitCodeOverride = 0 promptUserToConfigureSshConfigStub.resetHistory() }) diff --git a/packages/core/src/test/shared/telemetry/util.test.ts b/packages/core/src/test/shared/telemetry/util.test.ts index 059d86a891e..62a36884c56 100644 --- a/packages/core/src/test/shared/telemetry/util.test.ts +++ b/packages/core/src/test/shared/telemetry/util.test.ts @@ -10,6 +10,8 @@ import { convertLegacy, getClientId, getUserAgent, + getUserAgentPairs, + userAgentPairsToString, hadClientIdOnStartup, platformPair, SessionId, @@ -289,6 +291,86 @@ describe('getUserAgent', function () { }) }) +describe('getUserAgentPairs', function () { + it('returns array of [name, version] pairs', function () { + const pairs = getUserAgentPairs() + assert.ok(Array.isArray(pairs)) + assert.strictEqual(pairs.length, 1, 'Should have exactly one pair by default') + assert.ok(Array.isArray(pairs[0]), 'Each pair should be an array') + assert.strictEqual(pairs[0].length, 2, 'Each pair should have exactly 2 elements') + assert.ok(pairs[0][0].includes('Toolkit-For-VSCode'), 'Should include toolkit name') + assert.strictEqual(pairs[0][1], extensionVersion, 'Should include extension version') + }) + + it('includes platform pair when opted in', function () { + const pairs = getUserAgentPairs({ includePlatform: true }) + assert.ok(pairs.length > 1, 'Should have more than one pair when platform is included') + const platformPairStr = platformPair() + const [platformName, platformVersion] = platformPairStr.split('/') + const foundPlatformPair = pairs.find((pair) => pair[0] === platformName && pair[1] === platformVersion) + assert.ok(foundPlatformPair, 'Should include platform pair') + }) + + it('includes ClientId pair when opted in', function () { + const pairs = getUserAgentPairs({ includeClientId: true }) + const clientIdPair = pairs.find((pair) => pair[0] === 'ClientId') + assert.ok(clientIdPair, 'Should include ClientId pair') + assert.ok(clientIdPair[1], 'ClientId should have a value') + }) + + it('includes both platform and ClientId when both opted in', function () { + const pairs = getUserAgentPairs({ includePlatform: true, includeClientId: true }) + assert.ok(pairs.length >= 3, 'Should have at least 3 pairs') + const clientIdPair = pairs.find((pair) => pair[0] === 'ClientId') + assert.ok(clientIdPair, 'Should include ClientId pair') + const platformPairStr = platformPair() + const [platformName] = platformPairStr.split('/') + const foundPlatformPair = pairs.find((pair) => pair[0] === platformName) + assert.ok(foundPlatformPair, 'Should include platform pair') + }) + + it('omits ClientId by default', function () { + const pairs = getUserAgentPairs() + const clientIdPair = pairs.find((pair) => pair[0] === 'ClientId') + assert.strictEqual(clientIdPair, undefined, 'Should not include ClientId by default') + }) + + it('omits platform by default', function () { + const pairs = getUserAgentPairs() + assert.strictEqual(pairs.length, 1, 'Should have exactly one pair when nothing is opted in') + }) +}) + +describe('userAgentPairsToString', function () { + it('converts pairs to string format', function () { + const pairs: [string, string][] = [ + ['LAMBDA-DEBUG', '1.0.0'], + ['AWS-Toolkit', '2.0'], + ] + const result = userAgentPairsToString(pairs) + assert.strictEqual(result, 'LAMBDA-DEBUG/1.0.0 AWS-Toolkit/2.0') + }) + + it('handles single pair', function () { + const pairs: [string, string][] = [['AWS-Toolkit-For-VSCode', extensionVersion]] + const result = userAgentPairsToString(pairs) + assert.strictEqual(result, `AWS-Toolkit-For-VSCode/${extensionVersion}`) + }) + + it('handles empty array', function () { + const pairs: [string, string][] = [] + const result = userAgentPairsToString(pairs) + assert.strictEqual(result, '') + }) + + it('matches getUserAgent output when using getUserAgentPairs', function () { + const userAgentString = getUserAgent({ includePlatform: true, includeClientId: true }) + const userAgentPairs = getUserAgentPairs({ includePlatform: true, includeClientId: true }) + const reconstructedString = userAgentPairsToString(userAgentPairs) + assert.strictEqual(reconstructedString, userAgentString, 'String conversion should match getUserAgent output') + }) +}) + describe('validateMetricEvent', function () { it('does not validate exempt metrics', function () { const metricEvent: MetricDatum = { diff --git a/packages/core/src/test/shared/utilities/functionUtils.test.ts b/packages/core/src/test/shared/utilities/functionUtils.test.ts index b675fe74feb..3ba11518414 100644 --- a/packages/core/src/test/shared/utilities/functionUtils.test.ts +++ b/packages/core/src/test/shared/utilities/functionUtils.test.ts @@ -4,7 +4,13 @@ */ import assert from 'assert' -import { once, onceChanged, debounce, oncePerUniqueArg } from '../../../shared/utilities/functionUtils' +import { + once, + onceChanged, + debounce, + oncePerUniqueArg, + onceChangedWithComparator, +} from '../../../shared/utilities/functionUtils' import { installFakeClock } from '../../testUtil' describe('functionUtils', function () { @@ -49,6 +55,36 @@ describe('functionUtils', function () { assert.strictEqual(counter, 3) }) + it('onceChangedWithComparator()', function () { + let counter = 0 + const credentialsEqual = ([prev]: [any], [current]: [any]) => { + if (!prev && !current) { + return true + } + if (!prev || !current) { + return false + } + return prev.accessKeyId === current.accessKeyId && prev.secretAccessKey === current.secretAccessKey + } + const fn = onceChangedWithComparator((creds: any) => void counter++, credentialsEqual) + + const creds1 = { accessKeyId: 'key1', secretAccessKey: 'secret1' } + const creds2 = { accessKeyId: 'key1', secretAccessKey: 'secret1' } + const creds3 = { accessKeyId: 'key2', secretAccessKey: 'secret2' } + + fn(creds1) + assert.strictEqual(counter, 1) + + fn(creds2) // Same values, should not execute + assert.strictEqual(counter, 1) + + fn(creds3) // Different values, should execute + assert.strictEqual(counter, 2) + + fn(creds3) // Same as previous, should not execute + assert.strictEqual(counter, 2) + }) + it('oncePerUniqueArg()', function () { let counter = 0 const fn = oncePerUniqueArg((s: string) => { diff --git a/packages/core/src/test/shared/vscode/env.test.ts b/packages/core/src/test/shared/vscode/env.test.ts index cf09d085e68..a71aca33e8d 100644 --- a/packages/core/src/test/shared/vscode/env.test.ts +++ b/packages/core/src/test/shared/vscode/env.test.ts @@ -5,13 +5,21 @@ import assert from 'assert' import path from 'path' -import { isCloudDesktop, getEnvVars, getServiceEnvVarConfig, isAmazonLinux2, isBeta } from '../../../shared/vscode/env' +import { + isCloudDesktop, + getEnvVars, + getServiceEnvVarConfig, + isAmazonLinux2, + isBeta, + hasSageMakerEnvVars, +} from '../../../shared/vscode/env' import { ChildProcess } from '../../../shared/utilities/processUtils' import * as sinon from 'sinon' import os from 'os' import fs from '../../../shared/fs/fs' import vscode from 'vscode' import { getComputeEnvType } from '../../../shared/telemetry/util' +import * as globals from '../../../shared/extensionGlobals' describe('env', function () { // create a sinon sandbox instance and instantiate in a beforeEach @@ -97,22 +105,355 @@ describe('env', function () { assert.strictEqual(isBeta(), expected) }) - it('isAmazonLinux2', function () { - sandbox.stub(process, 'platform').value('linux') - const versionStub = stubOsVersion('5.10.220-188.869.amzn2int.x86_64') - assert.strictEqual(isAmazonLinux2(), true) + describe('isAmazonLinux2', function () { + let fsExistsStub: sinon.SinonStub + let fsReadFileStub: sinon.SinonStub + let isWebStub: sinon.SinonStub + let platformStub: sinon.SinonStub + let osReleaseStub: sinon.SinonStub + let moduleLoadStub: sinon.SinonStub + + beforeEach(function () { + // Default stubs + platformStub = sandbox.stub(process, 'platform').value('linux') + osReleaseStub = stubOsVersion('5.10.220-188.869.amzn2int.x86_64') + isWebStub = sandbox.stub(globals, 'isWeb').returns(false) + + // Mock fs module + const fsMock = { + existsSync: sandbox.stub().returns(false), + readFileSync: sandbox.stub().returns(''), + } + fsExistsStub = fsMock.existsSync + fsReadFileStub = fsMock.readFileSync + + // Stub Module._load to intercept require calls + const Module = require('module') + moduleLoadStub = sandbox.stub(Module, '_load').callThrough() + moduleLoadStub.withArgs('fs').returns(fsMock) + }) + + it('returns false in web environment', function () { + isWebStub.returns(true) + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('returns false in SageMaker environment with SAGEMAKER_APP_TYPE', function () { + const originalValue = process.env.SAGEMAKER_APP_TYPE + process.env.SAGEMAKER_APP_TYPE = 'JupyterLab' + try { + assert.strictEqual(isAmazonLinux2(), false) + } finally { + if (originalValue === undefined) { + delete process.env.SAGEMAKER_APP_TYPE + } else { + process.env.SAGEMAKER_APP_TYPE = originalValue + } + } + }) + + it('returns false in SageMaker environment with SM_APP_TYPE', function () { + const originalValue = process.env.SM_APP_TYPE + process.env.SM_APP_TYPE = 'JupyterLab' + try { + assert.strictEqual(isAmazonLinux2(), false) + } finally { + if (originalValue === undefined) { + delete process.env.SM_APP_TYPE + } else { + process.env.SM_APP_TYPE = originalValue + } + } + }) + + it('returns false in SageMaker environment with SERVICE_NAME', function () { + const originalValue = process.env.SERVICE_NAME + process.env.SERVICE_NAME = 'SageMakerUnifiedStudio' + try { + assert.strictEqual(isAmazonLinux2(), false) + } finally { + if (originalValue === undefined) { + delete process.env.SERVICE_NAME + } else { + process.env.SERVICE_NAME = originalValue + } + } + }) + + it('returns false when /etc/os-release indicates Ubuntu in container', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Ubuntu" +VERSION="20.04.6 LTS (Focal Fossa)" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 20.04.6 LTS" +VERSION_ID="20.04" + `) + + // Even with AL2 kernel (host is AL2), should return false (container is Ubuntu) + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('returns false when /etc/os-release indicates Amazon Linux 2023', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Amazon Linux" +VERSION="2023" +ID="amzn" +ID_LIKE="fedora" +VERSION_ID="2023" +PLATFORM_ID="platform:al2023" +PRETTY_NAME="Amazon Linux 2023" + `) + + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('returns true when /etc/os-release indicates Amazon Linux 2', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Amazon Linux 2" +VERSION="2" +ID="amzn" +ID_LIKE="centos rhel fedora" +VERSION_ID="2" +PRETTY_NAME="Amazon Linux 2" + `) + + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('returns true when /etc/os-release has ID="amzn" and VERSION_ID="2"', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Amazon Linux" +VERSION="2" +ID="amzn" +VERSION_ID="2" + `) + + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('returns false when /etc/os-release indicates CentOS', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="CentOS Linux" +VERSION="7 (Core)" +ID="centos" +ID_LIKE="rhel fedora" +VERSION_ID="7" + `) + + // Even with AL2 kernel + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('falls back to kernel check when /etc/os-release does not exist', function () { + fsExistsStub.returns(false) + + // Test with AL2 kernel + assert.strictEqual(isAmazonLinux2(), true) + + // Test with non-AL2 kernel + osReleaseStub.returns('5.10.220-188.869.NOT_INTERNAL.x86_64') + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('falls back to kernel check when /etc/os-release read fails', function () { + fsExistsStub.returns(true) + fsReadFileStub.throws(new Error('Permission denied')) + + // Should fall back to kernel check + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('returns true with .amzn2. kernel pattern', function () { + fsExistsStub.returns(false) + osReleaseStub.returns('5.10.236-227.928.amzn2.x86_64') + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('returns true with .amzn2int. kernel pattern', function () { + fsExistsStub.returns(false) + osReleaseStub.returns('5.10.220-188.869.amzn2int.x86_64') + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('returns false with non-AL2 kernel', function () { + fsExistsStub.returns(false) + osReleaseStub.returns('5.15.0-91-generic') + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('returns false on non-Linux platforms', function () { + platformStub.value('darwin') + fsExistsStub.returns(false) + assert.strictEqual(isAmazonLinux2(), false) + + platformStub.value('win32') + assert.strictEqual(isAmazonLinux2(), false) + }) - versionStub.returns('5.10.236-227.928.amzn2.x86_64') - assert.strictEqual(isAmazonLinux2(), true) + it('returns false when container OS is different from host OS', function () { + // Scenario: Host is AL2 (kernel shows AL2) but container is Ubuntu + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Ubuntu" +VERSION="22.04" +ID=ubuntu +VERSION_ID="22.04" + `) + osReleaseStub.returns('5.10.220-188.869.amzn2int.x86_64') // AL2 kernel from host - versionStub.returns('5.10.220-188.869.NOT_INTERNAL.x86_64') - assert.strictEqual(isAmazonLinux2(), false) + // Should trust container OS over kernel + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('handles os-release with comments correctly', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +# This is a comment with VERSION_ID="2023" that should be ignored +NAME="Amazon Linux 2" +VERSION="2" +ID="amzn" +# Another comment with PLATFORM_ID="platform:al2023" +VERSION_ID="2" +PRETTY_NAME="Amazon Linux 2" + `) + + // Should correctly identify as AL2 despite comments containing AL2023 identifiers + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('handles os-release with quoted values correctly', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Amazon Linux 2" +VERSION='2' +ID=amzn +VERSION_ID="2" +PRETTY_NAME='Amazon Linux 2' + `) + + // Should correctly parse both single and double quoted values + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('handles os-release with empty lines and whitespace', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` + +NAME="Amazon Linux 2" + +VERSION="2" + ID="amzn" +VERSION_ID="2" + +PRETTY_NAME="Amazon Linux 2" + + `) + + // Should correctly parse despite empty lines and whitespace + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('rejects Amazon Linux 2023 even with misleading comments', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +# This comment mentions Amazon Linux 2 but should not affect parsing +NAME="Amazon Linux" +VERSION="2023" +ID="amzn" +# Comment with VERSION_ID="2" should be ignored +VERSION_ID="2023" +PLATFORM_ID="platform:al2023" +PRETTY_NAME="Amazon Linux 2023" + `) + + // Should correctly identify as AL2023 (not AL2) despite misleading comments + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('handles malformed os-release lines gracefully', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Amazon Linux 2" +VERSION="2" +ID="amzn" +INVALID_LINE_WITHOUT_EQUALS +=INVALID_LINE_STARTING_WITH_EQUALS +VERSION_ID="2" +PRETTY_NAME="Amazon Linux 2" + `) + + // Should correctly parse valid lines and ignore malformed ones + assert.strictEqual(isAmazonLinux2(), true) + }) + }) + + describe('hasSageMakerEnvVars', function () { + afterEach(function () { + // Clean up environment variables + delete process.env.SAGEMAKER_APP_TYPE + delete process.env.SAGEMAKER_INTERNAL_IMAGE_URI + delete process.env.STUDIO_LOGGING_DIR + delete process.env.SM_APP_TYPE + delete process.env.SM_INTERNAL_IMAGE_URI + delete process.env.SERVICE_NAME + }) + + it('returns true when SAGEMAKER_APP_TYPE is set', function () { + process.env.SAGEMAKER_APP_TYPE = 'JupyterLab' + assert.strictEqual(hasSageMakerEnvVars(), true) + }) + + it('returns true when SM_APP_TYPE is set', function () { + process.env.SM_APP_TYPE = 'JupyterLab' + assert.strictEqual(hasSageMakerEnvVars(), true) + }) + + it('returns true when SERVICE_NAME is SageMakerUnifiedStudio', function () { + process.env.SERVICE_NAME = 'SageMakerUnifiedStudio' + assert.strictEqual(hasSageMakerEnvVars(), true) + }) + + it('returns true when STUDIO_LOGGING_DIR contains /var/log/studio', function () { + process.env.STUDIO_LOGGING_DIR = '/var/log/studio/logs' + assert.strictEqual(hasSageMakerEnvVars(), true) + }) + + it('returns false when no SageMaker env vars are set', function () { + assert.strictEqual(hasSageMakerEnvVars(), false) + }) + + it('returns false when SERVICE_NAME is set but not SageMakerUnifiedStudio', function () { + process.env.SERVICE_NAME = 'SomeOtherService' + assert.strictEqual(hasSageMakerEnvVars(), false) + }) }) it('isCloudDesktop', async function () { + // Mock fs module for isAmazonLinux2() calls + const fsMock = { + existsSync: sandbox.stub().returns(false), + readFileSync: sandbox.stub().returns(''), + } + const fsExistsStub = fsMock.existsSync + + // Stub Module._load to intercept require calls + const Module = require('module') + const moduleLoadStub = sandbox.stub(Module, '_load').callThrough() + moduleLoadStub.withArgs('fs').returns(fsMock) + sandbox.stub(process, 'platform').value('linux') + sandbox.stub(globals, 'isWeb').returns(false) stubOsVersion('5.10.220-188.869.amzn2int.x86_64') + // Mock fs to return false so it falls back to kernel check (which should return true for AL2) + fsExistsStub.returns(false) + const runStub = sandbox.stub(ChildProcess.prototype, 'run').resolves({ exitCode: 0 } as any) assert.strictEqual(await isCloudDesktop(), true) @@ -121,29 +462,58 @@ describe('env', function () { }) describe('getComputeEnvType', async function () { + let fsExistsStub: sinon.SinonStub + let moduleLoadStub: sinon.SinonStub + + beforeEach(function () { + // Mock fs module for isAmazonLinux2() calls + const fsMock = { + existsSync: sandbox.stub().returns(false), + readFileSync: sandbox.stub().returns(''), + } + fsExistsStub = fsMock.existsSync + + // Stub Module._load to intercept require calls + const Module = require('module') + moduleLoadStub = sandbox.stub(Module, '_load').callThrough() + moduleLoadStub.withArgs('fs').returns(fsMock) + }) + it('cloudDesktop', async function () { sandbox.stub(process, 'platform').value('linux') sandbox.stub(vscode.env, 'remoteName').value('ssh-remote') + sandbox.stub(globals, 'isWeb').returns(false) stubOsVersion('5.10.220-188.869.amzn2int.x86_64') sandbox.stub(ChildProcess.prototype, 'run').resolves({ exitCode: 0 } as any) + // Mock fs to return false so it falls back to kernel check (which should return true for AL2) + fsExistsStub.returns(false) + assert.deepStrictEqual(await getComputeEnvType(), 'cloudDesktop-amzn') }) it('ec2-internal', async function () { sandbox.stub(process, 'platform').value('linux') sandbox.stub(vscode.env, 'remoteName').value('ssh-remote') + sandbox.stub(globals, 'isWeb').returns(false) stubOsVersion('5.10.220-188.869.amzn2int.x86_64') sandbox.stub(ChildProcess.prototype, 'run').resolves({ exitCode: 1 } as any) + // Mock fs to return false so it falls back to kernel check (which should return true for AL2) + fsExistsStub.returns(false) + assert.deepStrictEqual(await getComputeEnvType(), 'ec2-amzn') }) it('ec2', async function () { sandbox.stub(process, 'platform').value('linux') sandbox.stub(vscode.env, 'remoteName').value('ssh-remote') + sandbox.stub(globals, 'isWeb').returns(false) stubOsVersion('5.10.220-188.869.NOT_INTERNAL.x86_64') + // Mock fs to return false so it falls back to kernel check (which should return false for non-AL2) + fsExistsStub.returns(false) + assert.deepStrictEqual(await getComputeEnvType(), 'ec2') }) }) diff --git a/packages/core/src/test/ssmDocument/commands/deleteDocument.test.ts b/packages/core/src/test/ssmDocument/commands/deleteDocument.test.ts index c16b76be8c7..f79d217028d 100644 --- a/packages/core/src/test/ssmDocument/commands/deleteDocument.test.ts +++ b/packages/core/src/test/ssmDocument/commands/deleteDocument.test.ts @@ -9,7 +9,7 @@ import { DocumentItemNodeWriteable } from '../../../ssmDocument/explorer/documen import { SsmDocumentClient } from '../../../shared/clients/ssmDocumentClient' import { deleteDocument } from '../../../ssmDocument/commands/deleteDocument' import { RegistryItemNode } from '../../../ssmDocument/explorer/registryItemNode' -import { SSM } from 'aws-sdk' +import { DocumentFormat, DocumentIdentifier } from '@aws-sdk/client-ssm' import { getTestWindow } from '../../shared/vscode/window' import { stub } from '../../utilities/stubber' @@ -21,9 +21,9 @@ describe('deleteDocument', async function () { let spyExecuteCommand: sinon.SinonSpy const fakeName: string = 'testDocument' - const fakeDoc: SSM.Types.DocumentIdentifier = { + const fakeDoc: DocumentIdentifier = { Name: fakeName, - DocumentFormat: 'json', + DocumentFormat: DocumentFormat.JSON, DocumentType: 'Automation', Owner: 'Amazon', } diff --git a/packages/core/src/test/ssmDocument/commands/openDocumentItem.test.ts b/packages/core/src/test/ssmDocument/commands/openDocumentItem.test.ts index 8e5bb3029d3..a02403be4dc 100644 --- a/packages/core/src/test/ssmDocument/commands/openDocumentItem.test.ts +++ b/packages/core/src/test/ssmDocument/commands/openDocumentItem.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SSM } from 'aws-sdk' +import { DocumentFormat, DocumentIdentifier, GetDocumentResult } from '@aws-sdk/client-ssm' import assert from 'assert' import * as sinon from 'sinon' @@ -23,8 +23,8 @@ describe('openDocumentItem', async function () { sinon.restore() }) - const rawContent: SSM.Types.GetDocumentResult = { - DocumentFormat: 'json', + const rawContent: GetDocumentResult = { + DocumentFormat: DocumentFormat.JSON, DocumentType: 'Command', Name: 'testDocument', Content: `{ @@ -35,9 +35,9 @@ describe('openDocumentItem', async function () { }`, } - const fakeDoc: SSM.Types.DocumentIdentifier = { + const fakeDoc: DocumentIdentifier = { Name: 'testDocument', - DocumentFormat: 'json', + DocumentFormat: DocumentFormat.JSON, DocumentType: 'Command', Owner: 'Amazon', } @@ -65,7 +65,7 @@ describe('openDocumentItem', async function () { const documentNode = generateDocumentItemNode() const openTextDocumentStub = sinon.stub(vscode.workspace, 'openTextDocument') - await openDocumentItem(documentNode, fakeAwsContext, 'json') + await openDocumentItem(documentNode, fakeAwsContext, DocumentFormat.JSON) assert.strictEqual(openTextDocumentStub.getCall(0).args[0]?.content, rawContent.Content) assert.strictEqual(openTextDocumentStub.getCall(0).args[0]?.language, 'ssm-json') }) diff --git a/packages/core/src/test/ssmDocument/commands/publishDocument.test.ts b/packages/core/src/test/ssmDocument/commands/publishDocument.test.ts index 1403848b9ee..c715d11ad18 100644 --- a/packages/core/src/test/ssmDocument/commands/publishDocument.test.ts +++ b/packages/core/src/test/ssmDocument/commands/publishDocument.test.ts @@ -3,7 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SSM } from 'aws-sdk' +import { + CreateDocumentRequest, + CreateDocumentResult, + UpdateDocumentRequest, + UpdateDocumentResult, +} from '@aws-sdk/client-ssm' import assert from 'assert' import * as sinon from 'sinon' @@ -24,15 +29,15 @@ import { SeverityLevel } from '../../shared/vscode/message' describe('publishDocument', async function () { let wizardResponse: PublishSSMDocumentWizardResponse let textDocument: vscode.TextDocument - let result: SSM.CreateDocumentResult | SSM.UpdateDocumentResult + let result: CreateDocumentResult | UpdateDocumentResult - const fakeCreateRequest: SSM.CreateDocumentRequest = { + const fakeCreateRequest: CreateDocumentRequest = { Content: 'foo', DocumentFormat: 'JSON', DocumentType: 'Automation', Name: 'test', } - const fakeUpdateRequest: SSM.UpdateDocumentRequest = { + const fakeUpdateRequest: UpdateDocumentRequest = { Content: 'foo', DocumentFormat: 'JSON', DocumentVersion: '$LATEST', diff --git a/packages/core/src/test/ssmDocument/commands/updateDocumentVersion.test.ts b/packages/core/src/test/ssmDocument/commands/updateDocumentVersion.test.ts index 01a21a9ee41..da941e43d62 100644 --- a/packages/core/src/test/ssmDocument/commands/updateDocumentVersion.test.ts +++ b/packages/core/src/test/ssmDocument/commands/updateDocumentVersion.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SSM } from 'aws-sdk' +import { DocumentFormat, DocumentIdentifier, DocumentVersionInfo } from '@aws-sdk/client-ssm' import * as sinon from 'sinon' import assert from 'assert' @@ -21,9 +21,9 @@ describe('openDocumentItem', async function () { sinon.restore() }) - const fakeDoc: SSM.Types.DocumentIdentifier = { + const fakeDoc: DocumentIdentifier = { Name: 'testDocument', - DocumentFormat: 'json', + DocumentFormat: DocumentFormat.JSON, DocumentType: 'Command', Owner: 'Amazon', } @@ -32,7 +32,7 @@ describe('openDocumentItem', async function () { const fakeRegion = 'us-east-1' - const fakeSchemaList: SSM.DocumentVersionInfo[] = [ + const fakeSchemaList: DocumentVersionInfo[] = [ { Name: 'testDocument', DocumentVersion: '1', diff --git a/packages/core/src/test/ssmDocument/explorer/documentItemNode.test.ts b/packages/core/src/test/ssmDocument/explorer/documentItemNode.test.ts index 12c400c4cef..3fa5dedef21 100644 --- a/packages/core/src/test/ssmDocument/explorer/documentItemNode.test.ts +++ b/packages/core/src/test/ssmDocument/explorer/documentItemNode.test.ts @@ -4,14 +4,14 @@ */ import assert from 'assert' -import { SSM } from 'aws-sdk' +import { DocumentIdentifier } from '@aws-sdk/client-ssm' import { DefaultSsmDocumentClient } from '../../../shared/clients/ssmDocumentClient' import { DocumentItemNode } from '../../../ssmDocument/explorer/documentItemNode' import { stub } from '../../utilities/stubber' describe('DocumentItemNode', async function () { let testNode: DocumentItemNode - const testDoc: SSM.DocumentIdentifier = { + const testDoc: DocumentIdentifier = { Name: 'testDoc', Owner: 'Amazon', } diff --git a/packages/core/src/test/utilities/fakeAwsContext.ts b/packages/core/src/test/utilities/fakeAwsContext.ts index 521fcde3cc4..d256980572c 100644 --- a/packages/core/src/test/utilities/fakeAwsContext.ts +++ b/packages/core/src/test/utilities/fakeAwsContext.ts @@ -57,6 +57,10 @@ export class FakeAwsContext implements AwsContext { public getCredentialDefaultRegion(): string { return this.awsContextCredentials?.defaultRegion ?? defaultRegion } + + public getCredentialEndpointUrl(): string | undefined { + return this.awsContextCredentials?.endpointUrl + } } export function makeFakeAwsContextWithPlaceholderIds(credentials: AWS.Credentials): FakeAwsContext { diff --git a/packages/core/src/testE2E/cloudformation/lspIntegration.test.ts b/packages/core/src/testE2E/cloudformation/lspIntegration.test.ts new file mode 100644 index 00000000000..dea201d561c --- /dev/null +++ b/packages/core/src/testE2E/cloudformation/lspIntegration.test.ts @@ -0,0 +1,322 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import * as vscode from 'vscode' +import * as path from 'path' +import * as os from 'os' +import { mkdtemp, rm, writeFile } from 'fs/promises' + +describe('CloudFormation LSP Integration E2E', function () { + let testDir: string + + before(async function () { + const envPath = process.env.__CLOUDFORMATIONLSP_PATH + console.log(`__CLOUDFORMATIONLSP_PATH = ${envPath}`) + + if (envPath) { + console.log(`Using local LSP server from: ${envPath}`) + } else { + console.log('No __CLOUDFORMATIONLSP_PATH set, will download LSP from GitHub') + } + + const extension = vscode.extensions.getExtension('amazonwebservices.aws-toolkit-vscode') + console.log(`Extension found: ${!!extension}, isActive: ${extension?.isActive}`) + if (extension && !extension.isActive) { + console.log('Activating extension...') + await extension.activate() + console.log('Extension activated') + } + + testDir = await mkdtemp(path.join(os.tmpdir(), 'cfn-lsp-test-')) + console.log('Waiting for LSP server to be ready...') + await new Promise((resolve) => setTimeout(resolve, 10000)) + console.log('Lsp wait time over...') + }) + + after(async function () { + await vscode.commands.executeCommand('workbench.action.closeAllEditors') + try { + await rm(testDir, { recursive: true, force: true }) + } catch (error) { + console.warn('Failed to clean up test directory:', error) + } + }) + + async function createTestFile( + filename: string, + content: string + ): Promise<{ uri: vscode.Uri; doc: vscode.TextDocument }> { + const filePath = path.join(testDir, filename) + await writeFile(filePath, content, 'utf-8') + const uri = vscode.Uri.file(filePath) + const doc = await vscode.workspace.openTextDocument(uri) + await vscode.window.showTextDocument(doc) + await new Promise((resolve) => setTimeout(resolve, 500)) + return { uri, doc } + } + + async function closeDocument(doc: vscode.TextDocument): Promise { + await vscode.window.showTextDocument(doc) + await vscode.commands.executeCommand('workbench.action.closeActiveEditor') + } + + describe('Autocomplete', function () { + it('should provide autocomplete for CloudFormation top-level sections', async function () { + const content = 'AWSTemplateFormatVersion: "2010-09-09"\n' + const { uri, doc } = await createTestFile('top-level.yaml', content) + + const completions = await vscode.commands.executeCommand( + 'vscode.executeCompletionItemProvider', + uri, + new vscode.Position(1, 0) + ) + + assert.ok(completions, 'Should receive completion items') + assert.ok(completions.items.length > 0, 'Should have completion items') + + const labels = completions.items.map((i) => (typeof i.label === 'string' ? i.label : i.label.label)) + const cfnSections = ['Description', 'Resources', 'Parameters', 'Outputs', 'Metadata'] + const found = labels.filter((label) => cfnSections.some((section) => label.includes(section))) + + assert.ok(found.length > 0, `Should have CloudFormation sections. Got: ${labels.join(', ')}`) + await closeDocument(doc) + }) + + it('should provide autocomplete for resource types', async function () { + const content = `AWSTemplateFormatVersion: "2010-09-09" +Resources: + MyBucket: + Type: ` + const { uri, doc } = await createTestFile('resource-type.yaml', content) + + const completions = await vscode.commands.executeCommand( + 'vscode.executeCompletionItemProvider', + uri, + new vscode.Position(3, 10) + ) + + assert.ok(completions, 'Should receive completion items') + const labels = completions.items.map((i) => (typeof i.label === 'string' ? i.label : i.label.label)) + const hasResourceType = labels.some((label) => label.includes('AWS::AccessAnalyzer::Analyzer')) + + assert.ok( + hasResourceType, + `Should have resource types in completions. Got: ${labels.slice(0, 10).join(', ')}` + ) + await closeDocument(doc) + }) + + it('should provide autocomplete for resource properties', async function () { + const content = `AWSTemplateFormatVersion: "2010-09-09" +Resources: + MyBucket: + Type: AWS::S3::Bucket + Properties: + ` + const { uri, doc } = await createTestFile('resource-props.yaml', content) + + const completions = await vscode.commands.executeCommand( + 'vscode.executeCompletionItemProvider', + uri, + new vscode.Position(5, 6) + ) + + assert.ok(completions, 'Should receive completion items') + const labels = completions.items.map((i) => (typeof i.label === 'string' ? i.label : i.label.label)) + const hasBucketName = labels.some((label) => label.includes('BucketName')) + + assert.ok(hasBucketName, `Should have BucketName in completions. Got: ${labels.join(', ')}`) + await closeDocument(doc) + }) + + it('should provide autocomplete for intrinsic functions', async function () { + const content = `AWSTemplateFormatVersion: "2010-09-09" +Parameters: + MyParam: + Type: String +Resources: + MyBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !` + const { uri, doc } = await createTestFile('intrinsic.yaml', content) + + const completions = await vscode.commands.executeCommand( + 'vscode.executeCompletionItemProvider', + uri, + new vscode.Position(8, 19) + ) + + assert.ok(completions, 'Should receive completion items') + const labels = completions.items.map((i) => (typeof i.label === 'string' ? i.label : i.label.label)) + const hasRef = labels.some((label) => label.includes('Ref')) + + assert.ok(hasRef, `Should have Ref in completions. Got: ${labels.join(', ')}`) + await closeDocument(doc) + }) + }) + + describe('Hover', function () { + it('should provide hover documentation for resource types', async function () { + const content = `AWSTemplateFormatVersion: "2010-09-09" +Resources: + MyBucket: + Type: AWS::S3::Bucket` + const { uri, doc } = await createTestFile('hover-resource.yaml', content) + + const hovers = await vscode.commands.executeCommand( + 'vscode.executeHoverProvider', + uri, + new vscode.Position(3, 15) + ) + + assert.ok(hovers && hovers.length > 0, 'Should receive hover information') + const hoverText = hovers[0].contents.map((c) => (typeof c === 'string' ? c : c.value)).join(' ') + assert.ok(hoverText.length > 0, 'Hover should contain documentation') + await closeDocument(doc) + }) + + it('should provide hover documentation for properties', async function () { + const content = `AWSTemplateFormatVersion: "2010-09-09" +Resources: + MyBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: my-bucket` + const { uri, doc } = await createTestFile('hover-property.yaml', content) + + const hovers = await vscode.commands.executeCommand( + 'vscode.executeHoverProvider', + uri, + new vscode.Position(5, 10) + ) + + assert.ok(hovers && hovers.length > 0, 'Should receive hover information') + await closeDocument(doc) + }) + + it('should provide hover documentation for intrinsic functions', async function () { + const content = `AWSTemplateFormatVersion: "2010-09-09" +Parameters: + MyParam: + Type: String +Resources: + MyBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref MyParam` + const { uri, doc } = await createTestFile('hover-intrinsic.yaml', content) + + const hovers = await vscode.commands.executeCommand( + 'vscode.executeHoverProvider', + uri, + new vscode.Position(8, 20) + ) + + assert.ok(hovers && hovers.length > 0, 'Should receive hover information for !Ref') + await closeDocument(doc) + }) + }) + + describe('Definition', function () { + it('should navigate to parameter definition from Ref', async function () { + const content = `AWSTemplateFormatVersion: "2010-09-09" +Parameters: + MyParam: + Type: String +Resources: + MyBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref MyParam` + const { uri, doc } = await createTestFile('definition-param.yaml', content) + + const definitions = await vscode.commands.executeCommand( + 'vscode.executeDefinitionProvider', + uri, + new vscode.Position(8, 25) + ) + + assert.ok(definitions && definitions.length > 0, 'Should find parameter definition') + assert.strictEqual(definitions[0].uri.toString(), uri.toString(), 'Definition should be in same file') + await closeDocument(doc) + }) + + it('should navigate to resource definition from GetAtt', async function () { + const content = `AWSTemplateFormatVersion: "2010-09-09" +Resources: + MyBucket: + Type: AWS::S3::Bucket + MyTopic: + Type: AWS::SNS::Topic + Properties: + DisplayName: !GetAtt MyBucket.Arn` + const { uri, doc } = await createTestFile('definition-resource.yaml', content) + + const definitions = await vscode.commands.executeCommand( + 'vscode.executeDefinitionProvider', + uri, + new vscode.Position(7, 30) + ) + + assert.ok(definitions && definitions.length > 0, 'Should find resource definition') + await closeDocument(doc) + }) + }) + + describe('Document Symbols', function () { + it('should provide document outline with resources', async function () { + const content = `AWSTemplateFormatVersion: "2010-09-09" +Description: Test template +Parameters: + MyParam: + Type: String +Resources: + MyBucket: + Type: AWS::S3::Bucket + MyTopic: + Type: AWS::SNS::Topic +Outputs: + BucketName: + Value: !Ref MyBucket` + const { uri, doc } = await createTestFile('symbols.yaml', content) + + const symbols = await vscode.commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + uri + ) + + assert.ok(symbols && symbols.length > 0, 'Should receive document symbols') + const symbolNames = symbols.map((s) => s.name) + assert.ok( + symbolNames.some((name) => name.includes('Resources') || name.includes('MyBucket')), + `Should have Resources or resource names in symbols. Got: ${symbolNames.join(', ')}` + ) + await closeDocument(doc) + }) + + it('should provide hierarchical symbols for nested structures', async function () { + const content = `AWSTemplateFormatVersion: "2010-09-09" +Resources: + MyBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: my-bucket + Tags: + - Key: Name + Value: MyBucket` + const { uri, doc } = await createTestFile('symbols-nested.yaml', content) + + const symbols = await vscode.commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + uri + ) + + assert.ok(symbols && symbols.length > 0, 'Should receive document symbols') + await closeDocument(doc) + }) + }) +}) diff --git a/packages/core/src/testE2E/cloudformation/setup-local-lsp.sh b/packages/core/src/testE2E/cloudformation/setup-local-lsp.sh new file mode 100755 index 00000000000..b77026f53a1 --- /dev/null +++ b/packages/core/src/testE2E/cloudformation/setup-local-lsp.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Script to setup local CloudFormation LSP server for E2E tests +# Downloads the latest LSP server release + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" + +LSP_DIR="$REPO_ROOT/.lsp-server" + +echo "Setting up CloudFormation LSP server for E2E tests..." + +# Detect platform and architecture +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m) + +case "$OS" in + darwin) PLATFORM="darwin" ;; + linux) PLATFORM="linux" ;; + mingw*|msys*|cygwin*) PLATFORM="win32" ;; + *) echo "Unsupported OS: $OS"; exit 1 ;; +esac + +case "$ARCH" in + x86_64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; +esac + +NODE_VERSION="22" + +# Fetch latest release +echo "Fetching latest LSP server release..." +MANIFEST_URL="https://raw.githubusercontent.com/aws-cloudformation/cloudformation-languageserver/main/assets/release-manifest.json" + +# Try manifest first +if command -v jq &> /dev/null; then + echo "Trying manifest: $MANIFEST_URL" + DOWNLOAD_URL=$(curl -s "$MANIFEST_URL" | jq -r ".prod[] | select(.latest == true) | .targets[] | select(.platform == \"$PLATFORM\" and .arch == \"$ARCH\" and .nodejs == \"$NODE_VERSION\") | .contents[0].url") + if [ -n "$DOWNLOAD_URL" ]; then + echo "✓ Using manifest URL" + fi +else + echo "jq not available, skipping manifest" +fi + +# Fallback to GitHub API if manifest fails +if [ -z "$DOWNLOAD_URL" ]; then + echo "Trying GitHub API fallback..." + RELEASE_URL="https://api.github.com/repos/aws-cloudformation/cloudformation-languageserver/releases/latest" + DOWNLOAD_URL=$(curl -s "$RELEASE_URL" | grep "browser_download_url.*${PLATFORM}-${ARCH}-node${NODE_VERSION}.zip" | cut -d'"' -f4 | head -1) + if [ -n "$DOWNLOAD_URL" ]; then + echo "✓ Using GitHub API URL" + fi +fi + +if [ -z "$DOWNLOAD_URL" ]; then + echo "Error: Could not find LSP server release for ${PLATFORM}-${ARCH}-node${NODE_VERSION}" + exit 1 +fi + +# Clean and recreate directory +rm -rf "$LSP_DIR" +mkdir -p "$LSP_DIR" +cd "$LSP_DIR" + +# Download and extract +echo "Downloading: $DOWNLOAD_URL" +curl -sL -o lsp-server.zip "$DOWNLOAD_URL" +unzip -q lsp-server.zip +rm lsp-server.zip + +# Find the actual LSP server file +LSP_FILE=$(find . -name "cfn-lsp-server-standalone.js" | head -1) +if [ -z "$LSP_FILE" ]; then + echo "Error: cfn-lsp-server-standalone.js not found in extracted files" + exit 1 +fi + +LSP_SERVER_DIR=$(dirname "$LSP_FILE") +LSP_SERVER_DIR=$(cd "$LSP_SERVER_DIR" && pwd) +echo "Found LSP server at: $LSP_SERVER_DIR" + +# Verify required files +REQUIRED_FILES=("cfn-lsp-server-standalone.js" "package.json" "pyodide-worker.js" "node_modules" "assets" "bin") +for file in "${REQUIRED_FILES[@]}"; do + if [ ! -e "$LSP_SERVER_DIR/$file" ]; then + echo "Warning: $file not found in $LSP_SERVER_DIR" + fi +done + +echo "" +echo "✓ LSP server ready at: $LSP_SERVER_DIR" +echo "" + +# Export to GitHub Actions environment if running in CI +if [ -n "$GITHUB_ENV" ]; then + # Convert to Windows path format if on Windows + if [[ "$PLATFORM" == "win32" ]]; then + # Convert /d/path to D:/path format for Node.js on Windows + WIN_PATH=$(echo "$LSP_SERVER_DIR" | sed 's|^/\([a-z]\)/|\U\1:/|') + echo "__CLOUDFORMATIONLSP_PATH=$WIN_PATH" >> "$GITHUB_ENV" + echo "Exported __CLOUDFORMATIONLSP_PATH=$WIN_PATH to GitHub Actions environment" + else + echo "__CLOUDFORMATIONLSP_PATH=$LSP_SERVER_DIR" >> "$GITHUB_ENV" + echo "Exported __CLOUDFORMATIONLSP_PATH=$LSP_SERVER_DIR to GitHub Actions environment" + fi +fi + +echo "Run tests with:" +echo "__CLOUDFORMATIONLSP_PATH=\"$LSP_SERVER_DIR\" npm run testE2E -w packages/toolkit" diff --git a/packages/core/src/testE2E/codecatalyst/client.test.ts b/packages/core/src/testE2E/codecatalyst/client.test.ts index 0356e3041c1..84e9309120c 100644 --- a/packages/core/src/testE2E/codecatalyst/client.test.ts +++ b/packages/core/src/testE2E/codecatalyst/client.test.ts @@ -20,7 +20,7 @@ import globals from '../../shared/extensionGlobals' import { CodeCatalystCreateWebview, SourceResponse } from '../../codecatalyst/vue/create/backend' import { waitUntil } from '../../shared/utilities/timeoutUtils' import { AccessDeniedException } from '@aws-sdk/client-sso-oidc' -import { GetDevEnvironmentRequest } from 'aws-sdk/clients/codecatalyst' +import { GetDevEnvironmentRequest, _InstanceType } from '@aws-sdk/client-codecatalyst' import { getTestWindow } from '../../test/shared/vscode/window' import { patchObject, registerAuthHook, skipTest, using } from '../../test/setupUtil' import { isExtensionInstalled } from '../../shared/utilities/vsCodeUtils' @@ -37,7 +37,6 @@ import { SsoConnection, } from '../../auth/connection' import { hasKey } from '../../shared/utilities/tsUtils' -import { _InstanceType } from '@aws-sdk/client-codecatalyst' let spaceName: CodeCatalystOrg['name'] let projectName: CodeCatalystProject['name'] @@ -615,6 +614,9 @@ describe('Test how this codebase uses the CodeCatalyst API', function () { ): Promise { const result = await waitUntil( async function () { + if (!devEnv.spaceName || !devEnv.projectName) { + return false + } const devEnvData = await client.getDevEnvironment({ spaceName: devEnv.spaceName, projectName: devEnv.projectName, diff --git a/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts b/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts new file mode 100644 index 00000000000..d173500c608 --- /dev/null +++ b/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts @@ -0,0 +1,124 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as codewhispererClient from '../../codewhisperer/client/codewhisperer' +import { ConfigurationEntry } from '../../codewhisperer/models/model' +import { setValidConnection, skipTestIfNoValidConn } from '../util/connection' +import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' +import { createMockTextEditor, resetCodeWhispererGlobalVariables } from '../../test/codewhisperer/testUtil' +import { invokeRecommendation } from '../../codewhisperer/commands/invokeRecommendation' +import { session } from '../../codewhisperer/util/codeWhispererSession' + +/* +New model deployment may impact references returned. +These tests: + 1) are not required for github approval flow + 2) will be auto-skipped until fix for manual runs is posted. +*/ + +const leftContext = `InAuto.GetContent( + InAuto.servers.auto, "vendors.json", + function (data) { + let block = ''; + for(let i = 0; i < data.length; i++) { + block += '' + cars[i].title + ''; + } + $('#cars').html(block); + });` + +describe('CodeWhisperer service invocation', async function () { + let validConnection: boolean + const client = new codewhispererClient.DefaultCodeWhispererClient() + const configWithRefs: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + const configWithNoRefs: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: false, + } + + before(async function () { + validConnection = await setValidConnection() + }) + + beforeEach(function () { + void resetCodeWhispererGlobalVariables() + RecommendationHandler.instance.clearRecommendations() + // TODO: remove this line (this.skip()) when these tests no longer auto-skipped + this.skip() + // valid connection required to run tests + skipTestIfNoValidConn(validConnection, this) + }) + + it('trigger known to return recs with references returns rec with reference', async function () { + // check that handler is empty before invocation + const requestIdBefore = RecommendationHandler.instance.requestId + const sessionIdBefore = session.sessionId + const validRecsBefore = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestIdBefore.length === 0) + assert.ok(sessionIdBefore.length === 0) + assert.ok(!validRecsBefore) + + const doc = leftContext + rightContext + const filename = 'test.js' + const language = 'javascript' + const line = 5 + const character = 39 + const mockEditor = createMockTextEditor(doc, filename, language, line, character) + + await invokeRecommendation(mockEditor, client, configWithRefs) + + const requestId = RecommendationHandler.instance.requestId + const sessionId = session.sessionId + const validRecs = RecommendationHandler.instance.isValidResponse() + const references = session.recommendations[0].references + + assert.ok(requestId.length > 0) + assert.ok(sessionId.length > 0) + assert.ok(validRecs) + assert.ok(references !== undefined) + // TODO: uncomment this assert when this test is no longer auto-skipped + // assert.ok(references.length > 0) + }) + + // This test will fail if user is logged in with IAM identity center + it('trigger known to return rec with references does not return rec with references when reference tracker setting is off', async function () { + // check that handler is empty before invocation + const requestIdBefore = RecommendationHandler.instance.requestId + const sessionIdBefore = session.sessionId + const validRecsBefore = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestIdBefore.length === 0) + assert.ok(sessionIdBefore.length === 0) + assert.ok(!validRecsBefore) + + const doc = leftContext + rightContext + const filename = 'test.js' + const language = 'javascript' + const line = 5 + const character = 39 + const mockEditor = createMockTextEditor(doc, filename, language, line, character) + + await invokeRecommendation(mockEditor, client, configWithNoRefs) + + const requestId = RecommendationHandler.instance.requestId + const sessionId = session.sessionId + const validRecs = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestId.length > 0) + assert.ok(sessionId.length > 0) + // no recs returned because example request returns 1 rec with reference, so no recs returned when references off + assert.ok(!validRecs) + }) +}) diff --git a/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts b/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts new file mode 100644 index 00000000000..37f32b130dd --- /dev/null +++ b/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts @@ -0,0 +1,124 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as vscode from 'vscode' +import * as path from 'path' +import { setValidConnection, skipTestIfNoValidConn } from '../util/connection' +import { ConfigurationEntry } from '../../codewhisperer/models/model' +import * as codewhispererClient from '../../codewhisperer/client/codewhisperer' +import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' +import { + createMockTextEditor, + createTextDocumentChangeEvent, + resetCodeWhispererGlobalVariables, +} from '../../test/codewhisperer/testUtil' +import { KeyStrokeHandler } from '../../codewhisperer/service/keyStrokeHandler' +import { sleep } from '../../shared/utilities/timeoutUtils' +import { invokeRecommendation } from '../../codewhisperer/commands/invokeRecommendation' +import { getTestWorkspaceFolder } from '../../testInteg/integrationTestsUtilities' +import { session } from '../../codewhisperer/util/codeWhispererSession' + +describe('CodeWhisperer service invocation', async function () { + let validConnection: boolean + const client = new codewhispererClient.DefaultCodeWhispererClient() + const config: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + + before(async function () { + validConnection = await setValidConnection() + }) + + beforeEach(function () { + void resetCodeWhispererGlobalVariables() + RecommendationHandler.instance.clearRecommendations() + // valid connection required to run tests + skipTestIfNoValidConn(validConnection, this) + }) + + it('manual trigger returns valid recommendation response', async function () { + // check that handler is empty before invocation + const requestIdBefore = RecommendationHandler.instance.requestId + const sessionIdBefore = session.sessionId + const validRecsBefore = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestIdBefore.length === 0) + assert.ok(sessionIdBefore.length === 0) + assert.ok(!validRecsBefore) + + const mockEditor = createMockTextEditor() + await invokeRecommendation(mockEditor, client, config) + + const requestId = RecommendationHandler.instance.requestId + const sessionId = session.sessionId + const validRecs = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestId.length > 0) + assert.ok(sessionId.length > 0) + assert.ok(validRecs) + }) + + it('auto trigger returns valid recommendation response', async function () { + // check that handler is empty before invocation + const requestIdBefore = RecommendationHandler.instance.requestId + const sessionIdBefore = session.sessionId + const validRecsBefore = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestIdBefore.length === 0) + assert.ok(sessionIdBefore.length === 0) + assert.ok(!validRecsBefore) + + const mockEditor = createMockTextEditor() + + const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( + mockEditor.document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), + '\n' + ) + + await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, client, config) + // wait for 5 seconds to allow time for response to be generated + await sleep(5000) + + const requestId = RecommendationHandler.instance.requestId + const sessionId = session.sessionId + const validRecs = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestId.length > 0) + assert.ok(sessionId.length > 0) + assert.ok(validRecs) + }) + + it('invocation in unsupported language does not generate a request', async function () { + const workspaceFolder = getTestWorkspaceFolder() + const appRoot = path.join(workspaceFolder, 'go1-plain-sam-app') + const appCodePath = path.join(appRoot, 'hello-world', 'go.mod') + + // check that handler is empty before invocation + const requestIdBefore = RecommendationHandler.instance.requestId + const sessionIdBefore = session.sessionId + const validRecsBefore = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestIdBefore.length === 0) + assert.ok(sessionIdBefore.length === 0) + assert.ok(!validRecsBefore) + + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(appCodePath)) + const editor = await vscode.window.showTextDocument(doc) + await invokeRecommendation(editor, client, config) + + const requestId = RecommendationHandler.instance.requestId + const sessionId = session.sessionId + const validRecs = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestId.length === 0) + assert.ok(sessionId.length === 0) + assert.ok(!validRecs) + }) +}) diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/hello_world/Dockerfile b/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/hello_world/Dockerfile new file mode 100644 index 00000000000..63816e8fb4d --- /dev/null +++ b/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/hello_world/Dockerfile @@ -0,0 +1,8 @@ +FROM public.ecr.aws/lambda/python:3.14 + +COPY app.py requirements.txt ./ + +RUN python3.14 -m pip install -r requirements.txt + +# Command can be overwritten by providing a different command in the template directly. +CMD ["app.lambda_handler"] \ No newline at end of file diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/hello_world/__init__.py b/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/hello_world/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/hello_world/app.py b/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/hello_world/app.py new file mode 100644 index 00000000000..139af7d44e2 --- /dev/null +++ b/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/hello_world/app.py @@ -0,0 +1,44 @@ +import json + +# import requests + + +def lambda_handler(event, context): + """Sample pure Lambda function + + Parameters + ---------- + event: dict, required + API Gateway Lambda Proxy Input Format + + Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + + context: object, required + Lambda Context runtime methods and attributes + + Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + + Returns + ------ + API Gateway Lambda Proxy Output Format: dict + + Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + """ + + # try: + # ip = requests.get("http://checkip.amazonaws.com/") + # except requests.RequestException as e: + # # Send some context about this error to Lambda Logs + # print(e) + + # raise e + + return { + "statusCode": 200, + "body": json.dumps( + { + "message": "hello world", + # "location": ip.text.replace("\n", "") + } + ), + } diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/hello_world/requirements.txt b/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/hello_world/requirements.txt new file mode 100644 index 00000000000..663bd1f6a2a --- /dev/null +++ b/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/hello_world/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/template.yaml b/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/template.yaml new file mode 100644 index 00000000000..ff108b4ce9b --- /dev/null +++ b/packages/core/src/testFixtures/workspaceFolder/python3.14-image-sam-app/template.yaml @@ -0,0 +1,30 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + python3.14-image-sam-app + + Sample SAM Template for python3.14-image-sam-app + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 3 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + PackageType: Image + # ImageConfig: + # Uncomment this to override command here from the Dockerfile + # Command: ["app.lambda_handler"] + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + Metadata: + DockerTag: python3.14-v1 + DockerContext: ./hello_world + Dockerfile: Dockerfile diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/events/event.json b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/events/event.json new file mode 100644 index 00000000000..9fbdd9ac098 --- /dev/null +++ b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/events/event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"message\": \"hello world\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/hello_world/__init__.py b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/hello_world/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/hello_world/app.py b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/hello_world/app.py new file mode 100644 index 00000000000..2da567d9afc --- /dev/null +++ b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/hello_world/app.py @@ -0,0 +1,65 @@ +import json + +# Comment is intentional to simulate real-world code. +# import requests + + +def lambda_handler(event, context): + """Sample pure Lambda function + + Parameters + ---------- + event: dict, required + API Gateway Lambda Proxy Input Format + + Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + + context: object, required + Lambda Context runtime methods and attributes + + Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + + Returns + ------ + API Gateway Lambda Proxy Output Format: dict + + Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + """ + + # Comment is intentional to simulate real-world code. + # try: + # ip = requests.get("http://checkip.amazonaws.com/") + # except requests.RequestException as e: + # # Send some context about this error to Lambda Logs + # print(e) + + # raise e + + return { + "statusCode": 200, + "body": json.dumps({ + "message": "hello world", + # Comment is intentional to simulate real-world code. + # "location": ip.text.replace("\n", "") + }), + } + +def lambda_handler_2(event, context): + return { + "statusCode": 200, + "body": json.dumps({ + "message": "hello from handler 2", + # Comment is intentional to simulate real-world code. + # "location": ip.text.replace("\n", "") + }), + } + +def lambda_handler_3(event, context): + return { + "statusCode": 200, + "body": json.dumps({ + "message": "hello from handler 3", + # Comment is intentional to simulate real-world code. + # "location": ip.text.replace("\n", "") + }), + } diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/hello_world/requirements.txt b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/hello_world/requirements.txt new file mode 100644 index 00000000000..663bd1f6a2a --- /dev/null +++ b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/hello_world/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/template.yaml b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/template.yaml new file mode 100644 index 00000000000..4566c0f7ab9 --- /dev/null +++ b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/template.yaml @@ -0,0 +1,61 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + python3.14-plain-sam-app + + Sample SAM Template for python3.14-plain-sam-app + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 3 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: hello_world/ + Handler: app.lambda_handler + Runtime: python3.14 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + Function2NotInLaunchJson: # Not in workspace's launch.json. + Type: AWS::Serverless::Function + Properties: + CodeUri: hello_world/ + Handler: app.lambda_handler_2 + Runtime: python3.14 + Function3NotInLaunchJson: # Not in workspace's launch.json. + Type: AWS::Serverless::Function + Properties: + CodeUri: hello_world/ + Handler: app.lambda_handler_3 + Runtime: python3.14 + Events: + HelloWorld: + Type: Api + Properties: + Path: /apipath1 + Method: get + ServerlessApi: + Type: AWS::Serverless::Api + Properties: + Name: ResourceName + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: 'API Gateway endpoint URL for Prod stage for Hello World function' + Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/' + HelloWorldFunction: + Description: 'Hello World Lambda Function ARN' + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: 'Implicit IAM Role created for Hello World function' + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/tests/unit/__init__.py b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/tests/unit/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/tests/unit/test_handler.py b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/tests/unit/test_handler.py new file mode 100644 index 00000000000..09588d3778e --- /dev/null +++ b/packages/core/src/testFixtures/workspaceFolder/python3.14-plain-sam-app/tests/unit/test_handler.py @@ -0,0 +1,73 @@ +import json + +import pytest + +from hello_world import app + + +@pytest.fixture() +def apigw_event(): + """ Generates API GW Event""" + + return { + "body": '{ "test": "body"}', + "resource": "/{proxy+}", + "requestContext": { + "resourceId": "123456", + "apiId": "1234567890", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "accountId": "123456789012", + "identity": { + "apiKey": "", + "userArn": "", + "cognitoAuthenticationType": "", + "caller": "", + "userAgent": "Custom User Agent String", + "user": "", + "cognitoIdentityPoolId": "", + "cognitoIdentityId": "", + "cognitoAuthenticationProvider": "", + "sourceIp": "127.0.0.1", + "accountId": "", + }, + "stage": "prod", + }, + "queryStringParameters": {"foo": "bar"}, + "headers": { + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "Accept-Language": "en-US,en;q=0.8", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Mobile-Viewer": "false", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "CloudFront-Viewer-Country": "US", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + "X-Forwarded-Port": "443", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "X-Forwarded-Proto": "https", + "X-Amz-Cf-Id": "aaaaaaaaaae3VYQb9jd-nvCd-de396Uhbp027Y2JvkCPNLmGJHqlaA==", + "CloudFront-Is-Tablet-Viewer": "false", + "Cache-Control": "max-age=0", + "User-Agent": "Custom User Agent String", + "CloudFront-Forwarded-Proto": "https", + "Accept-Encoding": "gzip, deflate, sdch", + }, + "pathParameters": {"proxy": "/examplepath"}, + "httpMethod": "POST", + "stageVariables": {"baz": "qux"}, + "path": "/examplepath", + } + + +def test_lambda_handler(apigw_event, mocker): + + ret = app.lambda_handler(apigw_event, "") + data = json.loads(ret["body"]) + + assert ret["statusCode"] == 200 + assert "message" in ret["body"] + assert data["message"] == "hello world" + # assert "location" in data.dict_keys() diff --git a/packages/core/src/testFixtures/workspaceFolder/remote-debug-ts-app/handler.js b/packages/core/src/testFixtures/workspaceFolder/remote-debug-ts-app/handler.js new file mode 100644 index 00000000000..21cbd8825a7 --- /dev/null +++ b/packages/core/src/testFixtures/workspaceFolder/remote-debug-ts-app/handler.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.handler = void 0; +const handler = async (event) => { + console.log('Event:', JSON.stringify(event, null, 2)); + return { + statusCode: 200, + body: JSON.stringify({ + message: 'Hello from TypeScript Lambda!' + }) + }; +}; +exports.handler = handler; +//# sourceMappingURL=handler.js.map diff --git a/packages/core/src/testFixtures/workspaceFolder/remote-debug-ts-app/handler.js.map b/packages/core/src/testFixtures/workspaceFolder/remote-debug-ts-app/handler.js.map new file mode 100644 index 00000000000..815020a2d30 --- /dev/null +++ b/packages/core/src/testFixtures/workspaceFolder/remote-debug-ts-app/handler.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "file": "handler.js", + "sourceRoot": "", + "sources": ["../../../../../../tmp5bmwuffn/handler.ts"], + "names": [], + "mappings": ";;;AAAA,MAAM,OAAO,GAAG,KAAK,EAAE,KAAU,EAAE,EAAE;IACjC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACtD,OAAO;QACH,UAAU,EAAE,GAAG;QACf,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACjB,OAAO,EAAE,+BAA+B;SAC3C,CAAC;KACL,CAAC;AACN,CAAC,CAAC;AAEF,OAAO,CAAC,OAAO,GAAG,OAAO,CAAC" +} diff --git a/packages/core/src/testInteg/appBuilder/serverlessLand/main.test.ts b/packages/core/src/testInteg/appBuilder/serverlessLand/main.test.ts index 7a443483440..6ca07ca691e 100644 --- a/packages/core/src/testInteg/appBuilder/serverlessLand/main.test.ts +++ b/packages/core/src/testInteg/appBuilder/serverlessLand/main.test.ts @@ -122,8 +122,11 @@ describe('Serverless Land Integration', async () => { // Validate Lambda resource configuration const lambdaResource = resourceNodes[0] as ResourceNode assert.strictEqual(lambdaResource.resource.resource.Type, 'AWS::Serverless::Function') - assert.strictEqual(lambdaResource.resource.resource.Runtime, 'dotnet8') + assert( + 'Runtime' in lambdaResource.resource.resource && lambdaResource.resource.resource.Runtime === 'dotnet8' + ) assert.strictEqual(lambdaResource.resource.resource.Id, 'HelloWorldFunction') + assert('Events' in lambdaResource.resource.resource) assert.deepStrictEqual(lambdaResource.resource.resource.Events, [ { Id: 'HelloWorld', @@ -132,6 +135,7 @@ describe('Serverless Land Integration', async () => { Method: 'get', }, ]) + assert('Environment' in lambdaResource.resource.resource) assert.deepStrictEqual(lambdaResource.resource.resource.Environment, { Variables: { PARAM1: 'VALUE', diff --git a/packages/core/src/testInteg/appBuilder/sidebar/appBuilderNode.test.ts b/packages/core/src/testInteg/appBuilder/sidebar/appBuilderNode.test.ts index bb5cdc4cc34..cd9416f0156 100644 --- a/packages/core/src/testInteg/appBuilder/sidebar/appBuilderNode.test.ts +++ b/packages/core/src/testInteg/appBuilder/sidebar/appBuilderNode.test.ts @@ -129,28 +129,28 @@ describe('Application Builder', async () => { ) assert.strictEqual(lambdaResourceNode.id, 'AppBuilderProjectLambda') const lambdaTreeItemProperties = lambdaResourceNode.getTreeItem() - assert.strictEqual(lambdaTreeItemProperties.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.strictEqual(lambdaTreeItemProperties.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) assert.strictEqual(lambdaTreeItemProperties.iconPath?.toString(), '$(aws-lambda-function)') // Validate s3 bucket const s3BucketResourceNode = getResourceNodeByType(appBuilderTestAppResourceNodes, 'AWS::S3::Bucket') assert.strictEqual(s3BucketResourceNode.id, 'AppBuilderProjectBucket') const s3BucketTreeItemProperties = s3BucketResourceNode.getTreeItem() - assert.strictEqual(s3BucketTreeItemProperties.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.strictEqual(s3BucketTreeItemProperties.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) assert.strictEqual(s3BucketTreeItemProperties.iconPath?.toString(), '$(aws-s3-bucket)') // Validate s3 policy const s3PolicyResourceNode = getResourceNodeByType(appBuilderTestAppResourceNodes, 'AWS::S3::BucketPolicy') assert.strictEqual(s3PolicyResourceNode.id, 'AppBuilderProjectBucketBucketPolicy') const s3PolicyTreeItemProperties = s3PolicyResourceNode.getTreeItem() - assert.strictEqual(s3PolicyTreeItemProperties.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.strictEqual(s3PolicyTreeItemProperties.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) assert.strictEqual(s3PolicyTreeItemProperties.iconPath?.toString(), '$(info)') // Validate api gateway resource node const apigwResourceNode = getResourceNodeByType(appBuilderTestAppResourceNodes, 'AWS::Serverless::Api') assert.strictEqual(apigwResourceNode.id, 'AppBuilderProjectAPI') const apigwTreeItemProperties = apigwResourceNode.getTreeItem() - assert.strictEqual(apigwTreeItemProperties.collapsibleState, vscode.TreeItemCollapsibleState.None) + assert.strictEqual(apigwTreeItemProperties.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed) assert.strictEqual(apigwTreeItemProperties.iconPath?.toString(), '$(info)') }) diff --git a/packages/core/src/testInteg/sam.test.ts b/packages/core/src/testInteg/sam.test.ts index 8dd8b5cdb9b..4f80d550df8 100644 --- a/packages/core/src/testInteg/sam.test.ts +++ b/packages/core/src/testInteg/sam.test.ts @@ -4,7 +4,7 @@ */ import assert from 'assert' -import { Runtime } from 'aws-sdk/clients/lambda' +import { Runtime } from '@aws-sdk/client-lambda' import { mkdtempSync } from 'fs' // eslint-disable-line no-restricted-imports import * as path from 'path' import * as semver from 'semver' @@ -92,7 +92,7 @@ const dotnetDefaults = { vscodeMinimum: '1.80.0', } -const defaults: Record = { +const defaults: Record = { nodejs: nodeDefaults, java: javaDefaults, python: pythonDefaults, @@ -100,7 +100,7 @@ const defaults: Record = { } function generateScenario( - runtime: Runtime, + runtime: string, version: string, options: Partial = {}, fromImage: boolean = false @@ -110,7 +110,7 @@ function generateScenario( } const { sourceTag, ...defaultOverride } = options const source = `(${options.sourceTag ? `${options.sourceTag} ` : ''}${fromImage ? 'Image' : 'ZIP'})` - const fullName = `${runtime}${version}` + const fullName = `${runtime}${version}` as Runtime return { runtime: fullName, displayName: `${fullName} ${source}`, @@ -123,7 +123,6 @@ function generateScenario( const scenarios: TestScenario[] = [ // zips - generateScenario('nodejs', '18.x'), generateScenario('nodejs', '20.x'), generateScenario('nodejs', '22.x', { vscodeMinimum: '1.78.0' }), generateScenario('python', '3.10'), @@ -135,7 +134,6 @@ const scenarios: TestScenario[] = [ generateScenario('java', '11', { sourceTag: 'Gradle' }), generateScenario('java', '17', { sourceTag: 'Gradle' }), // images - generateScenario('nodejs', '18.x', { baseImage: 'amazon/nodejs18.x-base' }, true), generateScenario('nodejs', '20.x', { baseImage: 'amazon/nodejs20.x-base' }, true), generateScenario('nodejs', '22.x', { baseImage: 'amazon/nodejs22.x-base', vscodeMinimum: '1.78.0' }, true), generateScenario('python', '3.10', { baseImage: 'amazon/python3.10-base' }, true), diff --git a/packages/core/src/testInteg/schema/schema.test.ts b/packages/core/src/testInteg/schema/schema.test.ts index aa70989d163..b6021141073 100644 --- a/packages/core/src/testInteg/schema/schema.test.ts +++ b/packages/core/src/testInteg/schema/schema.test.ts @@ -3,9 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import globals from '../../shared/extensionGlobals' -import { GlobalStorage } from '../../shared/globalStorage' -import { getDefaultSchemas, samAndCfnSchemaUrl } from '../../shared/schemas' import { getCITestSchemas, JSONObject, @@ -15,9 +12,6 @@ import { assertRef, assertDefinition, } from '../../test/shared/schema/testUtils' -import { assertTelemetry } from '../../test/testUtil' -import { waitUntil } from '../../shared/utilities/timeoutUtils' -import { fs } from '../../shared' describe('Sam Schema Regression', function () { let samSchema: JSONObject @@ -67,31 +61,3 @@ describe('Sam Schema Regression', function () { assertDefinitionProperty(samSchema, 'AWS::Serverless::Api.Auth', 'AddDefaultAuthorizerToCorsPreflight') }) }) - -describe('getDefaultSchemas()', () => { - beforeEach(async () => {}) - - it('uses cache after initial fetch for CFN/SAM schema', async () => { - await fs.delete(GlobalStorage.samAndCfnSchemaDestinationUri().fsPath) - await globals.telemetry.setTelemetryEnabled(true) - globals.telemetry.clearRecords() - globals.telemetry.logger.clear() - await getDefaultSchemas() - await getDefaultSchemas() - await getDefaultSchemas() - await waitUntil( - async () => { - return await fs.exists(GlobalStorage.samAndCfnSchemaDestinationUri().fsPath) - }, - { truthy: true, interval: 200, timeout: 5000 } - ) - assertTelemetry('toolkit_getExternalResource', [ - // Initial retrieval. - // (Technically, this is done on activation, not any of the getDefaultSchemas() calls above.) - { url: samAndCfnSchemaUrl, passive: true, result: 'Succeeded' }, - // Use cache after initial fetch. - { url: samAndCfnSchemaUrl, passive: true, result: 'Cancelled', reason: 'Cache hit' }, - { url: samAndCfnSchemaUrl, passive: true, result: 'Cancelled', reason: 'Cache hit' }, - ]) - }) -}) diff --git a/packages/toolkit/.c8rc.json b/packages/toolkit/.c8rc.json index 9702d17bc89..3e2bf48136f 100644 --- a/packages/toolkit/.c8rc.json +++ b/packages/toolkit/.c8rc.json @@ -2,5 +2,11 @@ "report-dir": "../../coverage/toolkit", "reporter": ["lcov"], "all": true, - "exclude": ["**/test*/**", "**/node_modules/**", "**/ssmServer.js", "**/ssmDocument/external *"] + "exclude": [ + "**/test*/**", + "**/node_modules/**", + "**/ssmServer.js", + "**/ssmDocument/external *", + "**/cloudformation-languageserver/**" + ] } diff --git a/packages/toolkit/.changes/3.72.0.json b/packages/toolkit/.changes/3.72.0.json new file mode 100644 index 00000000000..352b80850ee --- /dev/null +++ b/packages/toolkit/.changes/3.72.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-08-22", + "version": "3.72.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.73.0.json b/packages/toolkit/.changes/3.73.0.json new file mode 100644 index 00000000000..12676252824 --- /dev/null +++ b/packages/toolkit/.changes/3.73.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-09-05", + "version": "3.73.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.74.0.json b/packages/toolkit/.changes/3.74.0.json new file mode 100644 index 00000000000..001efa81cb9 --- /dev/null +++ b/packages/toolkit/.changes/3.74.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-09-10", + "version": "3.74.0", + "entries": [ + { + "type": "Feature", + "description": "Feature to support the access of SageMakerUnified Studio resources from the local VSCode IDE" + }, + { + "type": "Feature", + "description": "AWS Toolkit now correctly uses the endpoint URL specified in the AWS config file for the selected profile" + }, + { + "type": "Feature", + "description": "Lambda AppBuilder: Now you can install LocalStack VS Code extension from the AppBuilder walkthrough" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.75.0.json b/packages/toolkit/.changes/3.75.0.json new file mode 100644 index 00000000000..a863028083b --- /dev/null +++ b/packages/toolkit/.changes/3.75.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-09-19", + "version": "3.75.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.76.0.json b/packages/toolkit/.changes/3.76.0.json new file mode 100644 index 00000000000..1b61d94d46d --- /dev/null +++ b/packages/toolkit/.changes/3.76.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-09-25", + "version": "3.76.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.77.0.json b/packages/toolkit/.changes/3.77.0.json new file mode 100644 index 00000000000..cd8e1686932 --- /dev/null +++ b/packages/toolkit/.changes/3.77.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-09-29", + "version": "3.77.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.78.0.json b/packages/toolkit/.changes/3.78.0.json new file mode 100644 index 00000000000..b0b05902c21 --- /dev/null +++ b/packages/toolkit/.changes/3.78.0.json @@ -0,0 +1,14 @@ +{ + "date": "2025-10-02", + "version": "3.78.0", + "entries": [ + { + "type": "Feature", + "description": "Refactor and optimize Lambda Remote Invoke UI with enhanced payload management" + }, + { + "type": "Feature", + "description": "Appbuilder now show local invoke icon on deployed local lambda node. Remote Debugging now auto detect sam, cdk outFiles for typescript debug." + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.79.0.json b/packages/toolkit/.changes/3.79.0.json new file mode 100644 index 00000000000..ce9c5531853 --- /dev/null +++ b/packages/toolkit/.changes/3.79.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-10-10", + "version": "3.79.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.80.0.json b/packages/toolkit/.changes/3.80.0.json new file mode 100644 index 00000000000..4db49741fa7 --- /dev/null +++ b/packages/toolkit/.changes/3.80.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-10-16", + "version": "3.80.0", + "entries": [ + { + "type": "Bug Fix", + "description": "The space is updated upon creation of a new app with the requested settings" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.81.0.json b/packages/toolkit/.changes/3.81.0.json new file mode 100644 index 00000000000..43f8ac55d1a --- /dev/null +++ b/packages/toolkit/.changes/3.81.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-10-22", + "version": "3.81.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.82.0.json b/packages/toolkit/.changes/3.82.0.json new file mode 100644 index 00000000000..a56b15acf24 --- /dev/null +++ b/packages/toolkit/.changes/3.82.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-10-30", + "version": "3.82.0", + "entries": [ + { + "type": "Feature", + "description": "Lambda AppBuilder: Now you can install Finch from the AppBuilder walkthrough" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.83.0.json b/packages/toolkit/.changes/3.83.0.json new file mode 100644 index 00000000000..fd904d0c9fe --- /dev/null +++ b/packages/toolkit/.changes/3.83.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-11-06", + "version": "3.83.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.84.0.json b/packages/toolkit/.changes/3.84.0.json new file mode 100644 index 00000000000..6c19d58d25b --- /dev/null +++ b/packages/toolkit/.changes/3.84.0.json @@ -0,0 +1,14 @@ +{ + "date": "2025-11-15", + "version": "3.84.0", + "entries": [ + { + "type": "Feature", + "description": "SageMaker: Improved UX for connecting to running spaces with better progress indicators and streamlined remote access handling" + }, + { + "type": "Feature", + "description": "Deeplink support for SageMaker Unified Studio" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.85.0.json b/packages/toolkit/.changes/3.85.0.json new file mode 100644 index 00000000000..eb1f7adf691 --- /dev/null +++ b/packages/toolkit/.changes/3.85.0.json @@ -0,0 +1,14 @@ +{ + "date": "2025-11-19", + "version": "3.85.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Lambda: Attaching a debugger to your Lambda functions using LocalStack is not working" + }, + { + "type": "Feature", + "description": "CloudFormation: Add comprehensive Language Server Protocol integration with stack management, deployment workflows, drift detection, and cfn-init project support" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.86.0.json b/packages/toolkit/.changes/3.86.0.json new file mode 100644 index 00000000000..aae928e9939 --- /dev/null +++ b/packages/toolkit/.changes/3.86.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-11-21", + "version": "3.86.0", + "entries": [ + { + "type": "Feature", + "description": "Remote IDE connection support for IDE Spaces deployed on SageMaker HyperPod clusters" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.87.0.json b/packages/toolkit/.changes/3.87.0.json new file mode 100644 index 00000000000..4d1f36aa4ca --- /dev/null +++ b/packages/toolkit/.changes/3.87.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-11-21", + "version": "3.87.0", + "entries": [ + { + "type": "Feature", + "description": "Support IAM based domains for SageMaker Unified Studio" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.88.0.json b/packages/toolkit/.changes/3.88.0.json new file mode 100644 index 00000000000..1a5011c7a70 --- /dev/null +++ b/packages/toolkit/.changes/3.88.0.json @@ -0,0 +1,22 @@ +{ + "date": "2025-11-22", + "version": "3.88.0", + "entries": [ + { + "type": "Bug Fix", + "description": "CloudFormation: refresh stacks after change set deletion" + }, + { + "type": "Bug Fix", + "description": "CloudFormation: Handle telemetry setting in upgrade path case where setting is not registered" + }, + { + "type": "Bug Fix", + "description": "CloudFormation: prevent eager loading of CloudFormation stacks" + }, + { + "type": "Feature", + "description": "Remote debugging now supports nodejs24.x, python3.14, java25" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.89.0.json b/packages/toolkit/.changes/3.89.0.json new file mode 100644 index 00000000000..77b520c148a --- /dev/null +++ b/packages/toolkit/.changes/3.89.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-11-25", + "version": "3.89.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.90.0.json b/packages/toolkit/.changes/3.90.0.json new file mode 100644 index 00000000000..d0818d8cb65 --- /dev/null +++ b/packages/toolkit/.changes/3.90.0.json @@ -0,0 +1,26 @@ +{ + "date": "2025-12-09", + "version": "3.90.0", + "entries": [ + { + "type": "Bug Fix", + "description": "SageMaker: SSH configuration errors now display line numbers and include an \"Open SSH Config\" button" + }, + { + "type": "Bug Fix", + "description": "SageMaker Unified Studio: Fixed s3 table catalog node showing error when it's empty" + }, + { + "type": "Bug Fix", + "description": "CloudFormation: hide deployment button when change set is not deployable, add delete button when change set has no changes" + }, + { + "type": "Feature", + "description": "CloudFormation: Shorten/simplify deployment prompts by prompting for deployment mode first" + }, + { + "type": "Feature", + "description": "feat(lambda): add support for lmi function resource node" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index 51beb2a13e5..176b8142caf 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,91 @@ +## 3.90.0 2025-12-09 + +- **Bug Fix** SageMaker: SSH configuration errors now display line numbers and include an "Open SSH Config" button +- **Bug Fix** SageMaker Unified Studio: Fixed s3 table catalog node showing error when it's empty +- **Bug Fix** CloudFormation: hide deployment button when change set is not deployable, add delete button when change set has no changes +- **Feature** CloudFormation: Shorten/simplify deployment prompts by prompting for deployment mode first +- **Feature** feat(lambda): add support for lmi function resource node + +## 3.89.0 2025-11-25 + +- Miscellaneous non-user-facing changes + +## 3.88.0 2025-11-22 + +- **Bug Fix** CloudFormation: refresh stacks after change set deletion +- **Bug Fix** CloudFormation: Handle telemetry setting in upgrade path case where setting is not registered +- **Bug Fix** CloudFormation: prevent eager loading of CloudFormation stacks +- **Feature** Remote debugging now supports nodejs24.x, python3.14, java25 + +## 3.87.0 2025-11-21 + +- **Feature** Support IAM based domains for SageMaker Unified Studio + +## 3.86.0 2025-11-21 + +- **Feature** Remote IDE connection support for IDE Spaces deployed on SageMaker HyperPod clusters + +## 3.85.0 2025-11-19 + +- **Bug Fix** Lambda: Attaching a debugger to your Lambda functions using LocalStack is not working +- **Feature** CloudFormation: Add comprehensive Language Server Protocol integration with stack management, deployment workflows, drift detection, and cfn-init project support + +## 3.84.0 2025-11-15 + +- **Feature** SageMaker: Improved UX for connecting to running spaces with better progress indicators and streamlined remote access handling +- **Feature** Deeplink support for SageMaker Unified Studio + +## 3.83.0 2025-11-06 + +- Miscellaneous non-user-facing changes + +## 3.82.0 2025-10-30 + +- **Feature** Lambda AppBuilder: Now you can install Finch from the AppBuilder walkthrough + +## 3.81.0 2025-10-22 + +- Miscellaneous non-user-facing changes + +## 3.80.0 2025-10-16 + +- **Bug Fix** The space is updated upon creation of a new app with the requested settings + +## 3.79.0 2025-10-10 + +- Miscellaneous non-user-facing changes + +## 3.78.0 2025-10-02 + +- **Feature** Refactor and optimize Lambda Remote Invoke UI with enhanced payload management +- **Feature** Appbuilder now show local invoke icon on deployed local lambda node. Remote Debugging now auto detect sam, cdk outFiles for typescript debug. + +## 3.77.0 2025-09-29 + +- Miscellaneous non-user-facing changes + +## 3.76.0 2025-09-25 + +- Miscellaneous non-user-facing changes + +## 3.75.0 2025-09-19 + +- Miscellaneous non-user-facing changes + +## 3.74.0 2025-09-10 + +- **Feature** Feature to support the access of SageMakerUnified Studio resources from the local VSCode IDE +- **Feature** AWS Toolkit now correctly uses the endpoint URL specified in the AWS config file for the selected profile +- **Feature** Lambda AppBuilder: Now you can install LocalStack VS Code extension from the AppBuilder walkthrough + +## 3.73.0 2025-09-05 + +- Miscellaneous non-user-facing changes + +## 3.72.0 2025-08-22 + +- Miscellaneous non-user-facing changes + ## 3.71.0 2025-08-06 - Miscellaneous non-user-facing changes diff --git a/packages/toolkit/cloudformation-language-config.json b/packages/toolkit/cloudformation-language-config.json new file mode 100644 index 00000000000..c5d33df3c93 --- /dev/null +++ b/packages/toolkit/cloudformation-language-config.json @@ -0,0 +1,53 @@ +{ + "comments": { + "lineComment": "#", + "blockComment": ["/*", "*/"] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"], + ["`", "`"] + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"], + ["`", "`"] + ], + "folding": { + "offSide": true, + "markers": { + "start": "^\\s*#\\s*region\\b", + "end": "^\\s*#\\s*endregion\\b" + } + }, + "indentationRules": { + "increaseIndentPattern": "^\\s*.*(:|-) ?(&\\w+)?(\\{[^}\"']*|\\([^)\"']*)?$", + "decreaseIndentPattern": "^\\s+\\}$" + }, + "onEnterRules": [ + { + "beforeText": "^\\s*\\w+:\\s*$", + "action": { + "indent": "indent" + } + }, + { + "beforeText": "^\\s*- \\w+:$", + "action": { + "indent": "indent" + } + } + ], + "wordPattern": "(^.?[^\\s]+)+|([^\\s\n={[][\\w\\-\\./$%&*:\"']+)" +} diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 9539121648e..4b5ef7b6f4d 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -2,7 +2,7 @@ "name": "aws-toolkit-vscode", "displayName": "AWS Toolkit", "description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.", - "version": "3.72.0-SNAPSHOT", + "version": "3.91.0-SNAPSHOT", "extensionKind": [ "workspace" ], @@ -46,6 +46,7 @@ "onLanguage:python", "onLanguage:csharp", "onLanguage:yaml", + "onLanguage:cloudformation", "onFileSystem:s3", "onFileSystem:s3-readonly" ], @@ -71,6 +72,7 @@ "testCompile": "npm run clean && npm run buildScripts && npm run compileOnly", "test": "npm run testCompile && c8 --allowExternal ts-node ../core/scripts/test/launchTest.ts unit dist/test/unit/index.js ../core/dist/src/testFixtures/workspaceFolder", "testE2E": "npm run testCompile && c8 --allowExternal ts-node ../core/scripts/test/launchTest.ts e2e dist/test/e2e/index.js ../core/dist/src/testFixtures/workspaceFolder", + "testE2ECfn": "npm run testCompile && c8 --allowExternal ts-node ../core/scripts/test/launchTest.ts e2e dist/test/e2e/cloudformation/index.js ../core/dist/src/testFixtures/workspaceFolder", "testInteg": "npm run testCompile && c8 --allowExternal ts-node ../core/scripts/test/launchTest.ts integration dist/test/integ/index.js ../core/dist/src/testFixtures/workspaceFolder", "package": "ts-node ../../scripts/package.ts", "lint": "true", @@ -240,13 +242,19 @@ "type": "object", "markdownDescription": "%AWS.configuration.description.experiments%", "default": { - "jsonResourceModification": false + "jsonResourceModification": false, + "cloudFormationService": false }, "properties": { "jsonResourceModification": { "type": "boolean", "default": false }, + "cloudFormationService": { + "type": "boolean", + "default": false, + "markdownDescription": "Enable the new CloudFormation language server and service features" + }, "amazonqLSP": { "type": "boolean", "default": true @@ -304,6 +312,224 @@ "type": "boolean", "default": false, "description": "Enable automatic filtration of spaces based on your AWS identity." + }, + "aws.cloudformation.telemetry.enabled": { + "type": "boolean", + "default": false, + "description": "Configure anonymous telemetry collection for AWS CloudFormation Language Server" + }, + "aws.cloudformation.hover.enabled": { + "type": "boolean", + "default": true, + "description": "Enable hover information for CloudFormation resources" + }, + "aws.cloudformation.completion.enabled": { + "type": "boolean", + "default": true, + "description": "Enable auto-completion for CloudFormation templates" + }, + "aws.cloudformation.diagnostics.cfnLint.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable CloudFormation linting" + }, + "aws.cloudformation.diagnostics.cfnLint.lintOnChange": { + "type": "boolean", + "default": true, + "description": "Run cfn-lint when document content changes" + }, + "aws.cloudformation.diagnostics.cfnLint.delayMs": { + "type": "number", + "default": 3000, + "minimum": 0, + "description": "Delay in milliseconds before running cfn-lint after changes" + }, + "aws.cloudformation.diagnostics.cfnLint.path": { + "type": "string", + "default": "", + "description": "Path to locally installed cfn-lint executable. If empty, uses bundled version." + }, + "aws.cloudformation.diagnostics.cfnLint.customization": { + "type": "object", + "default": { + "includeChecks": [ + "I" + ] + }, + "description": "CFN-Lint customization options", + "properties": { + "ignoreChecks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Rule IDs to ignore" + }, + "includeChecks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Only check these rule IDs" + }, + "mandatoryChecks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Always check these rules" + }, + "includeExperimental": { + "type": "boolean", + "description": "Include experimental rules" + }, + "configureRules": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Rule configurations (RuleId:key=value)" + }, + "regions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "AWS regions to validate against" + }, + "customRules": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Paths to custom rule files" + }, + "appendRules": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional rule directories" + }, + "overrideSpec": { + "type": "string", + "description": "CloudFormation spec override file path" + }, + "registrySchemas": { + "type": "array", + "items": { + "type": "string" + }, + "description": "CloudFormation Registry schema paths" + } + }, + "additionalProperties": false + }, + "aws.cloudformation.diagnostics.cfnGuard.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable CloudFormation Guard validation" + }, + "aws.cloudformation.diagnostics.cfnGuard.validateOnChange": { + "type": "boolean", + "default": true, + "description": "Run cfn-guard when document content changes" + }, + "aws.cloudformation.diagnostics.cfnGuard.enabledRulePacks": { + "type": "array", + "default": [ + "wa-Security-Pillar" + ], + "items": { + "type": "string", + "enum": [ + "ABS-CCIGv2-Material", + "ABS-CCIGv2-Standard", + "acsc-essential-8", + "acsc-ism", + "apra-cpg-234", + "bnm-rmit", + "cis-aws-benchmark-level-1", + "cis-aws-benchmark-level-2", + "cis-critical-security-controls-v8-ig1", + "cis-critical-security-controls-v8-ig2", + "cis-critical-security-controls-v8-ig3", + "cis-top-20", + "cisa-ce", + "cmmc-level-1", + "cmmc-level-2", + "cmmc-level-3", + "cmmc-level-4", + "cmmc-level-5", + "enisa-cybersecurity-guide-for-smes", + "ens-high", + "ens-low", + "ens-medium", + "FDA-21CFR-Part-11", + "FedRAMP-Low", + "FedRAMP-Moderate", + "ffiec", + "hipaa-security", + "K-ISMS", + "mas-notice-655", + "mas-trmg", + "nbc-trmg", + "ncsc-cafv3", + "ncsc", + "nerc", + "nist-1800-25", + "nist-800-171", + "nist-800-172", + "nist-800-181", + "nist-csf", + "nist-privacy-framework", + "NIST800-53Rev4", + "NIST800-53Rev5", + "nzism", + "PCI-DSS-3-2-1", + "rbi-bcsf-ucb", + "rbi-md-itf", + "us-nydfs", + "wa-Reliability-Pillar", + "wa-Security-Pillar" + ] + }, + "description": "Cfn-guard enabled rule packs" + }, + "aws.cloudformation.diagnostics.cfnGuard.rulesFile": { + "type": "string", + "default": "", + "description": "Path to custom cfn-guard rules file. If empty, uses default rule packs." + }, + "aws.cloudformation.s3": { + "type": "string", + "enum": [ + "alwaysAsk", + "alwaysUpload", + "neverUpload" + ], + "enumDescriptions": [ + "Always ask during validation and deploy workflow", + "Always upload to S3 for both validation and deployment", + "Never upload to S3 (only works for template smaller than 51200 bytes)" + ], + "default": "alwaysAsk", + "description": "Configure S3 upload behavior for CloudFormation templates" + }, + "aws.cloudformation.environment.saveOptions": { + "type": "string", + "enum": [ + "alwaysAsk", + "alwaysSave", + "neverSave" + ], + "enumDescriptions": [ + "Always ask during validation and deploy workflow", + "Always save to file for both validation and deployment", + "Never save to file" + ], + "default": "alwaysAsk", + "description": "Configure optional changeset flags for CloudFormation templates" } } }, @@ -722,6 +948,13 @@ } } } + ], + "panel": [ + { + "id": "cfn-diff", + "title": "CloudFormation", + "icon": "$(diff)" + } ] }, "viewsWelcome": [ @@ -737,6 +970,43 @@ } ], "views": { + "cfn-diff": [ + { + "id": "aws.cloudformation.stack.overview", + "name": "Overview", + "type": "webview", + "when": "aws.cloudformation.stackSelected && !aws.cloudformation.changeSetMode", + "icon": "$(info)" + }, + { + "id": "aws.cloudformation.stack.resources", + "name": "Resources", + "type": "webview", + "when": "aws.cloudformation.stackSelected && !aws.cloudformation.changeSetMode", + "icon": "$(symbol-class)" + }, + { + "id": "aws.cloudformation.stack.events", + "name": "Events", + "type": "webview", + "when": "aws.cloudformation.stackSelected && !aws.cloudformation.changeSetMode", + "icon": "$(history)" + }, + { + "id": "aws.cloudformation.stack.outputs", + "name": "Outputs", + "type": "webview", + "when": "aws.cloudformation.stackSelected && !aws.cloudformation.changeSetMode", + "icon": "$(output)" + }, + { + "id": "aws.cloudformation.diff", + "name": "Stack Changes", + "type": "webview", + "when": "aws.cloudformation.changeSetMode", + "icon": "$(diff)" + } + ], "explorer": [ { "id": "aws.appBuilderForFileExplorer", @@ -769,6 +1039,11 @@ "name": "%AWS.cdk.explorerTitle%", "when": "!aws.explorer.showAuthView" }, + { + "id": "aws.cloudformation", + "name": "CloudFormation", + "when": "!aws.explorer.showAuthView" + }, { "id": "aws.appBuilder", "name": "%AWS.appBuilder.explorerTitle%", @@ -779,6 +1054,11 @@ "name": "%AWS.codecatalyst.explorerTitle%", "when": "(!isCloud9 && !aws.isSageMaker || isCloud9CodeCatalyst) && !aws.explorer.showAuthView" }, + { + "id": "aws.smus.rootView", + "name": "%AWS.sagemakerunifiedstudio.explorerTitle%", + "when": "!aws.explorer.showAuthView" + }, { "type": "webview", "id": "aws.toolkit.AmazonCommonAuth", @@ -1257,6 +1537,50 @@ { "command": "aws.sagemaker.filterSpaceApps", "when": "false" + }, + { + "command": "aws.hyperpod.filterDevSpaces", + "when": "false" + }, + { + "command": "aws.smus.switchProject", + "when": "false" + }, + { + "command": "aws.smus.refreshProject", + "when": "false" + }, + { + "command": "aws.cloudformation.api.copyResourceIdentifier", + "when": "false" + }, + { + "command": "aws.cloudformation.api.searchResource", + "when": "false" + }, + { + "command": "aws.cloudformation.api.loadMoreChangeSets", + "when": "false" + }, + { + "command": "aws.cloudformation.stacks.refreshChangeSets", + "when": "false" + }, + { + "command": "aws.cloudformation.stacks.viewChangeSet", + "when": "false" + }, + { + "command": "aws.cloudformation.stacks.deleteChangeSet", + "when": "false" + }, + { + "command": "aws.cloudformation.api.deployTemplateFromStacksMenu", + "when": "false" + }, + { + "command": "aws.cloudformation.server.restartServer", + "when": "false" } ], "editor/title": [ @@ -1318,7 +1642,29 @@ "group": "1_cutcopypaste@1" } ], + "editor/context": [ + { + "command": "aws.cloudformation.api.rerunValidateAndDeploy", + "when": "resourceExtname == .yaml || resourceExtname == .json || resourceExtname == .yml || resourceExtname == .txt || resourceExtname == .cfn || resourceExtname == .template", + "group": "1_cloudformation@1" + } + ], "view/title": [ + { + "command": "aws.smus.switchProject", + "when": "view == aws.smus.rootView && !aws.isWebExtHost && aws.smus.connected && !aws.smus.inSmusSpaceEnvironment", + "group": "smus@0" + }, + { + "command": "aws.smus.refreshProject", + "when": "view == aws.smus.rootView && !aws.isWebExtHost && aws.smus.connected", + "group": "smus@1" + }, + { + "command": "aws.smus.signOut", + "when": "view == aws.smus.rootView && !aws.isWebExtHost && aws.smus.connected && !aws.smus.inSmusSpaceEnvironment", + "group": "smus@2" + }, { "command": "aws.toolkit.submitFeedback", "when": "view == aws.explorer && !aws.isWebExtHost", @@ -1460,18 +1806,48 @@ "command": "aws.stepfunctions.openWithWorkflowStudio", "when": "isFileSystemResource && resourceFilename =~ /^.*\\.asl\\.(json|yml|yaml)$/", "group": "z_aws@1" + }, + { + "command": "aws.smus.notebookscheduling.createjob", + "when": "resourceExtname == .ipynb", + "group": "z_aws@1" + }, + { + "command": "aws.smus.notebookscheduling.viewjobs", + "when": "resourceExtname == .ipynb", + "group": "z_aws@1" } ], "view/item/context": [ { "command": "aws.sagemaker.stopSpace", "group": "inline@0", - "when": "viewItem =~ /^(awsSagemakerSpaceRunningRemoteEnabledNode|awsSagemakerSpaceRunningRemoteDisabledNode)$/" + "when": "view != aws.smus.rootView && viewItem == awsSagemakerSpaceRunningNode" + }, + { + "command": "aws.smus.stopSpace", + "group": "inline@0", + "when": "view == aws.smus.rootView && viewItem == awsSagemakerSpaceRunningNode" }, { "command": "aws.sagemaker.openRemoteConnection", "group": "inline@1", - "when": "viewItem =~ /^(awsSagemakerSpaceRunningRemoteEnabledNode|awsSagemakerSpaceStoppedRemoteEnabledNode|awsSagemakerSpaceStoppedRemoteDisabledNode)$/" + "when": "view != aws.smus.rootView && viewItem =~ /^(awsSagemakerSpaceRunningNode|awsSagemakerSpaceStoppedNode)$/" + }, + { + "command": "aws.hyperpod.openRemoteConnection", + "group": "inline@0", + "when": "viewItem == awsSagemakerHyperpodDevSpaceStoppedNode || viewItem == awsSagemakerHyperpodDevSpaceRunningNode" + }, + { + "command": "aws.hyperpod.stopSpace", + "group": "inline@1", + "when": "viewItem == awsSagemakerHyperpodDevSpaceRunningNode" + }, + { + "command": "aws.smus.openRemoteConnection", + "group": "inline@1", + "when": "view == aws.smus.rootView && viewItem =~ /^(awsSagemakerSpaceRunningNode|awsSagemakerSpaceStoppedNode)$/" }, { "command": "_aws.toolkit.notifications.dismiss", @@ -1630,7 +2006,22 @@ }, { "command": "aws.sagemaker.filterSpaceApps", - "when": "view == aws.explorer && viewItem == awsSagemakerParentNode", + "when": "view == aws.explorer && viewItem == awsSagemakerStudioNode", + "group": "inline@1" + }, + { + "command": "aws.hyperpod.filterDevSpaces", + "when": "view == aws.explorer && viewItem == awsSagemakerHyperpodNode", + "group": "inline@1" + }, + { + "command": "aws.smus.switchProject", + "when": "view == aws.smus.rootView && viewItem == smusSelectedProject", + "group": "0_project@1" + }, + { + "command": "aws.smus.refreshProject", + "when": "view == aws.smus.rootView && viewItem == smusSelectedProject", "group": "inline@1" }, { @@ -1685,47 +2076,47 @@ }, { "command": "aws.invokeLambda", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/ || viewItem == awsAppBuilderDeployedNode", + "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly|awsCloudFormationFunctionNode|awsAppBuilderResourceNode.deployed-function)$/ || viewItem == awsAppBuilderDeployedNode", "group": "0@1" }, { "command": "aws.downloadLambda", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/ || viewItem == awsAppBuilderDeployedNode", + "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/ || viewItem == awsAppBuilderDeployedNode", "group": "0@2" }, { "command": "aws.lambda.openWorkspace", - "when": "view == aws.explorer && viewItem == awsRegionFunctionNodeDownloadable", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/", "group": "0@6" }, { "command": "aws.toolkit.lambda.convertToSam", - "when": "view == aws.explorer && viewItem == awsRegionFunctionNodeDownloadable", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNodeDownloadable)$/", "group": "0@3" }, { "command": "aws.uploadLambda", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/ || viewItem == awsAppBuilderDeployedNode", + "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/ || viewItem == awsAppBuilderDeployedNode", "group": "1@1" }, { "command": "aws.deleteLambda", - "when": "view =~ /^(aws.explorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/", + "when": "view =~ /^(aws.explorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/", "group": "4@1" }, { "command": "aws.copyLambdaUrl", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/ || viewItem == awsAppBuilderDeployedNode", + "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/ || viewItem == awsAppBuilderDeployedNode", "group": "2@0" }, { "command": "aws.appBuilder.searchLogs", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/", + "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly|awsCloudFormationFunctionNode|awsAppBuilderResourceNode.deployed-function)$/", "group": "0@3" }, { "command": "aws.appBuilder.tailLogs", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/", + "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly|awsCloudFormationFunctionNode)$/", "group": "0@4" }, { @@ -1815,7 +2206,12 @@ }, { "command": "aws.sagemaker.filterSpaceApps", - "when": "view == aws.explorer && viewItem == awsSagemakerParentNode", + "when": "view == aws.explorer && viewItem == awsSagemakerStudioNode", + "group": "0@1" + }, + { + "command": "aws.hyperpod.filterDevSpaces", + "when": "view == aws.explorer && viewItem == awsSagemakerHyperpodNode", "group": "0@1" }, { @@ -1870,17 +2266,17 @@ }, { "command": "aws.copyName", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|(awsEc2(Running|Pending|Stopped)Node))/", + "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|(awsEc2(Running|Pending|Stopped)Node))/", "group": "2@1" }, { "command": "aws.copyArn", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsCloudWatchLogNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsEcrRepositoryNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsEcsServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|awsMdeInstanceNode|(awsEc2(Running|Pending|Stopped)Node))/", + "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly|awsCloudFormationFunctionNode|awsStateMachineNode|awsCloudFormationNode|awsCloudWatchLogNode|awsS3BucketNode|awsS3FolderNode|awsS3FileNode|awsApiGatewayNode|awsEcrRepositoryNode|awsIotThingNode)$|^(awsAppRunnerServiceNode|awsEcsServiceNode|awsIotCertificateNode|awsIotPolicyNode|awsIotPolicyVersionNode|awsMdeInstanceNode|(awsEc2(Running|Pending|Stopped)Node)|awsCapacityProviderNode)/", "group": "2@2" }, { "command": "aws.openAwsConsole", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsS3BucketNode)$/", + "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly|awsS3BucketNode|awsCapacityProviderNode)$/", "group": "2@3" }, { @@ -2148,11 +2544,6 @@ "when": "viewItem == awsAppBuilderAppNode", "group": "inline@2" }, - { - "command": "aws.launchDebugConfigForm", - "when": "viewItem == awsAppBuilderResourceNode.function", - "group": "inline@1" - }, { "command": "aws.appBuilder.deploy", "when": "viewItem == awsAppBuilderAppNode", @@ -2169,10 +2560,30 @@ "group": "inline@1" }, { - "command": "aws.appBuilder.openHandler", - "when": "viewItem == awsAppBuilderResourceNode.function", + "command": "aws.launchDebugConfigForm", + "when": "viewItem == awsAppBuilderResourceNode.function || viewItem == awsAppBuilderResourceNode.deployed-function", "group": "inline@1" }, + { + "command": "aws.invokeLambda", + "when": "viewItem == awsAppBuilderResourceNode.deployed-function", + "group": "inline@2" + }, + { + "command": "aws.appBuilder.searchLogs", + "when": "viewItem == awsAppBuilderResourceNode.deployed-function", + "group": "inline@3" + }, + { + "command": "aws.appBuilder.openHandler", + "when": "viewItem == awsAppBuilderResourceNode.function || viewItem == awsAppBuilderResourceNode.deployed-function", + "group": "inline@4" + }, + { + "command": "aws.appBuilder.tailLogs", + "when": "viewItem == awsAppBuilderResourceNode.deployed-function", + "group": "0@5" + }, { "submenu": "aws.toolkit.auth", "when": "viewItem == awsAuthNode", @@ -2200,7 +2611,7 @@ }, { "command": "aws.appBuilder.openHandler", - "when": "viewItem == awsAppBuilderResourceNode.function", + "when": "viewItem == awsAppBuilderResourceNode.function|| viewItem == awsAppBuilderResourceNode.deployed-function", "group": "1@1" }, { @@ -2210,7 +2621,7 @@ }, { "command": "aws.launchDebugConfigForm", - "when": "viewItem == awsAppBuilderResourceNode.function", + "when": "viewItem == awsAppBuilderResourceNode.function || viewItem == awsAppBuilderResourceNode.deployed-function", "group": "1@2" }, { @@ -2225,22 +2636,22 @@ }, { "command": "aws.invokeLambda", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/", + "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly|awsCloudFormationFunctionNode)$/", "group": "inline@1" }, { "command": "aws.appBuilder.searchLogs", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/", + "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly|awsCloudFormationFunctionNode)$/", "group": "inline@2" }, { "command": "aws.quickDeployLambda", - "when": "view == aws.explorer && viewItem == awsRegionFunctionNodeDownloadable", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/", "group": "inline@3" }, { "command": "aws.toolkit.lambda.convertToSam", - "when": "view == aws.explorer && viewItem == awsRegionFunctionNodeDownloadable", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNodeDownloadable)$/", "group": "inline@4" }, { @@ -2314,23 +2725,193 @@ "when": "viewItem =~ /^awsDocDB-(?!cluster-global).*/" }, { - "command": "aws.docdb.viewConsole", - "when": "viewItem =~ /^awsDocDB-/", - "group": "0@1" + "command": "aws.docdb.viewConsole", + "when": "viewItem =~ /^awsDocDB-/", + "group": "0@1" + }, + { + "command": "aws.docdb.viewDocs", + "when": "viewItem == awsDocDBNode" + }, + { + "command": "aws.docdb.copyEndpoint", + "when": "viewItem =~ /^awsDocDB-/", + "group": "0@1" + }, + { + "command": "aws.appBuilder.tailLogs", + "when": "view =~ /^(aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly|awsCloudFormationFunctionNode)$/", + "group": "inline@3" + }, + { + "command": "aws.cloudformation.stack.view", + "when": "view == aws.cloudformation && viewItem == stack", + "group": "inline@3" + }, + { + "command": "aws.cloudformation.api.loadMoreStacks", + "when": "view == aws.cloudformation && viewItem == stackSectionWithMore && !aws.cloudformation.loadingStacks", + "group": "inline@4" + }, + { + "command": "aws.cloudformation.stacks.refresh", + "when": "view == aws.cloudformation && (viewItem == stackSection || viewItem == stackSectionWithMore) && !aws.cloudformation.refreshingStacks", + "group": "inline@3" + }, + { + "command": "aws.cloudformation.api.deployTemplateFromStacksMenu", + "when": "view == aws.cloudformation && (viewItem == stackSection || viewItem == stackSectionWithMore) && !aws.cloudformation.refreshingStacks", + "group": "inline@3" + }, + { + "command": "aws.cloudformation.api.deployTemplateFromStacksMenu", + "when": "view == aws.cloudformation && (viewItem == stackSection || viewItem == stackSectionWithMore) && !aws.cloudformation.refreshingStacks", + "group": "1@2" + }, + { + "command": "aws.cloudformation.api.loadMoreStacks", + "when": "view == aws.cloudformation && viewItem == stackSectionWithMore && !aws.cloudformation.loadingStacks", + "group": "1@1" + }, + { + "command": "aws.cloudformation.stacks.refresh", + "when": "view == aws.cloudformation && (viewItem == stackSection || viewItem == stackSectionWithMore) && !aws.cloudformation.refreshingStacks", + "group": "1@2" + }, + { + "command": "aws.cloudformation.api.deployTemplate", + "when": "view == aws.cloudformation && (viewItem == stack)", + "group": "inline@2" + }, + { + "command": "aws.cloudformation.api.deployTemplate", + "when": "view == aws.cloudformation && (viewItem == stack)", + "group": "1@2" + }, + { + "command": "aws.cloudformation.stack.view", + "when": "view == aws.cloudformation && viewItem == stack", + "group": "1@3" + }, + { + "command": "aws.cloudformation.selectRegion", + "when": "view == aws.cloudformation && viewItem == regionSelector", + "group": "inline" + }, + { + "command": "aws.cloudformation.api.addResourceTypes", + "when": "view == aws.cloudformation && viewItem == resourceSection && !aws.cloudformation.refreshingAllResources", + "group": "inline@1" + }, + { + "command": "aws.cloudformation.api.refreshAllResources", + "when": "view == aws.cloudformation && viewItem == resourceSection && !aws.cloudformation.refreshingAllResources", + "group": "inline@2" + }, + { + "command": "aws.cloudformation.api.loadMoreResources", + "when": "view == aws.cloudformation && viewItem == resourceTypeWithMore && !aws.cloudformation.loadingResources", + "group": "inline@3" + }, + { + "command": "aws.cloudformation.api.searchResource", + "when": "view == aws.cloudformation && viewItem == resourceTypeWithMore", + "group": "inline@1" + }, + { + "command": "aws.cloudformation.api.importResourceState", + "when": "view == aws.cloudformation && viewItem == resource && !aws.cloudformation.importingResource", + "group": "inline@1" + }, + { + "command": "aws.cloudformation.api.cloneResourceState", + "when": "view == aws.cloudformation && viewItem == resource && !aws.cloudformation.cloningResource", + "group": "inline@2" + }, + { + "command": "aws.cloudformation.api.getStackManagementInfo", + "when": "view == aws.cloudformation && viewItem == resource && !aws.cloudformation.gettingStackMgmtInfo", + "group": "inline@3" + }, + { + "command": "aws.cloudformation.api.refreshResourceList", + "when": "view == aws.cloudformation && (viewItem == resourceType || viewItem == resourceTypeWithMore) && !aws.cloudformation.refreshingResourceList", + "group": "inline@2" + }, + { + "command": "aws.cloudformation.api.importResourceState", + "when": "view == aws.cloudformation && viewItem == resource && !aws.cloudformation.importingResource", + "group": "1@1" + }, + { + "command": "aws.cloudformation.api.cloneResourceState", + "when": "view == aws.cloudformation && viewItem == resource && !aws.cloudformation.cloningResource", + "group": "1@2" + }, + { + "command": "aws.cloudformation.api.getStackManagementInfo", + "when": "view == aws.cloudformation && viewItem == resource && !listMultiSelection", + "group": "1@3" + }, + { + "command": "aws.cloudformation.api.copyResourceIdentifier", + "when": "view == aws.cloudformation && viewItem == resource && !listMultiSelection", + "group": "1@4" + }, + { + "command": "aws.cloudformation.api.searchResource", + "when": "view == aws.cloudformation && viewItem == resourceTypeWithMore", + "group": "1@1" + }, + { + "command": "aws.cloudformation.api.loadMoreResources", + "when": "view == aws.cloudformation && viewItem == resourceTypeWithMore && !aws.cloudformation.loadingResources", + "group": "1@2" + }, + { + "command": "aws.cloudformation.api.refreshResourceList", + "when": "view == aws.cloudformation && (viewItem == resourceType || viewItem == resourceTypeWithMore) && !aws.cloudformation.refreshingResourceList", + "group": "1@1" + }, + { + "command": "aws.cloudformation.removeResourceType", + "when": "view == aws.cloudformation && (viewItem == resourceType || viewItem == resourceTypeWithMore)", + "group": "1@2" + }, + { + "command": "aws.cloudformation.api.addResourceTypes", + "when": "view == aws.cloudformation && viewItem == resourceSection && !aws.cloudformation.refreshingAllResources", + "group": "1@1" + }, + { + "command": "aws.cloudformation.api.refreshAllResources", + "when": "view == aws.cloudformation && viewItem == resourceSection && !aws.cloudformation.refreshingAllResources", + "group": "1@2" + }, + { + "command": "aws.cloudformation.stacks.viewChangeSet", + "when": "view == aws.cloudformation && viewItem == changeSet", + "group": "inline@1" + }, + { + "command": "aws.cloudformation.stacks.viewChangeSet", + "when": "view == aws.cloudformation && viewItem == changeSet", + "group": "1@1" }, { - "command": "aws.docdb.viewDocs", - "when": "viewItem == awsDocDBNode" + "command": "aws.cloudformation.stacks.deleteChangeSet", + "when": "view == aws.cloudformation && viewItem == changeSet", + "group": "1@2" }, { - "command": "aws.docdb.copyEndpoint", - "when": "viewItem =~ /^awsDocDB-/", - "group": "0@1" + "command": "aws.cloudformation.stacks.refreshChangeSets", + "when": "view == aws.cloudformation && (viewItem == stackChangeSets || viewItem == stackChangeSetsWithMore)", + "group": "inline@1" }, { - "command": "aws.appBuilder.tailLogs", - "when": "view =~ /^(aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/", - "group": "inline@3" + "command": "aws.cloudformation.api.loadMoreChangeSets", + "when": "view == aws.cloudformation && viewItem == stackChangeSetsWithMore", + "group": "inline@2" } ], "aws.toolkit.auth": [ @@ -2389,6 +2970,11 @@ ] }, "commands": [ + { + "command": "aws.smus.openSpaceRemoteConnection", + "title": "Connect to SageMaker-Unified-Studio Space", + "icon": "$(remote-explorer)" + }, { "command": "_aws.toolkit.notifications.dismiss", "title": "%AWS.generic.dismiss%", @@ -2630,6 +3216,18 @@ } } }, + { + "command": "aws.hyperpod.filterDevSpaces", + "title": "Filter Hyperpod DevSpaces", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(extensions-filter)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, { "command": "aws.sagemaker.openRemoteConnection", "title": "Connect to SageMaker Space", @@ -2642,6 +3240,18 @@ } } }, + { + "command": "aws.smus.openRemoteConnection", + "title": "Connect to SageMaker Space", + "icon": "$(remote-explorer)", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, { "command": "aws.sagemaker.stopSpace", "title": "Stop SageMaker Space", @@ -2654,6 +3264,90 @@ } } }, + { + "command": "aws.hyperpod.openRemoteConnection", + "title": "Connect to HyperPod Space", + "icon": "$(remote-explorer)", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.hyperpod.stopSpace", + "title": "Stop HyperPod Space", + "icon": "$(debug-stop)", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.smus.stopSpace", + "title": "Stop SageMaker Space", + "icon": "$(debug-stop)", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.smus.switchProject", + "title": "%AWS.command.smus.switchProject%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.smus.refreshProject", + "title": "%AWS.command.smus.refreshProject%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": { + "dark": "resources/icons/vscode/dark/refresh.svg", + "light": "resources/icons/vscode/light/refresh.svg" + }, + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.smus.refresh", + "title": "%AWS.command.smus.refresh%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": { + "dark": "resources/icons/vscode/dark/refresh.svg", + "light": "resources/icons/vscode/light/refresh.svg" + } + }, + { + "command": "aws.smus.signOut", + "title": "%AWS.command.smus.signOut%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(sign-out)", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, { "command": "aws.ec2.startInstance", "title": "%AWS.command.ec2.startInstance%", @@ -3109,7 +3803,7 @@ "title": "%AWS.command.invokeLambda%", "category": "%AWS.title%", "enablement": "isCloud9 || !aws.isWebExtHost", - "icon": "$(play)", + "icon": "$(aws-lambda-invoke-remotely)", "cloud9": { "cn": { "title": "%AWS.command.invokeLambda.cn%", @@ -3121,7 +3815,7 @@ "command": "aws.toolkit.lambda.convertToSam", "title": "%AWS.command.lambda.convertToSam%", "category": "%AWS.title%", - "enablement": "viewItem == awsRegionFunctionNodeDownloadable", + "enablement": "viewItem =~ /^(awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/", "cloud9": { "cn": { "category": "%AWS.title.cn%" @@ -3136,7 +3830,7 @@ "command": "aws.downloadLambda", "title": "%AWS.command.downloadLambda%", "category": "%AWS.title%", - "enablement": "viewItem == awsRegionFunctionNodeDownloadable", + "enablement": "viewItem =~ /^(awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/", "cloud9": { "cn": { "category": "%AWS.title.cn%" @@ -3147,7 +3841,7 @@ "command": "aws.lambda.openWorkspace", "title": "%AWS.command.openLambdaWorkspace%", "category": "%AWS.title%", - "enablement": "viewItem == awsRegionFunctionNodeDownloadable", + "enablement": "viewItem =~ /^(awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/", "cloud9": { "cn": { "category": "%AWS.title.cn%" @@ -3177,7 +3871,7 @@ "command": "aws.quickDeployLambda", "title": "%AWS.command.quickDeployLambda%", "category": "%AWS.title%", - "enablement": "viewItem == awsRegionFunctionNodeDownloadable", + "enablement": "viewItem =~ /^(awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/", "cloud9": { "cn": { "category": "%AWS.title.cn%" @@ -4283,6 +4977,164 @@ "category": "%AWS.title.cn%" } } + }, + { + "command": "aws.smus.notebookscheduling.createjob", + "title": "Create Notebook Job", + "category": "Job" + }, + { + "command": "aws.smus.notebookscheduling.viewjobs", + "title": "View Notebook Jobs", + "category": "Job" + }, + { + "command": "aws.cloudformation.api.importResourceState", + "title": "Import Resource State", + "icon": "$(diff-added)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.copyResourceIdentifier", + "title": "Copy Resource Identifier", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.refreshResourceList", + "title": "Refresh Resource List", + "icon": "$(refresh)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.addResourceTypes", + "title": "Add Resource Types to List", + "icon": "$(add)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.removeResourceType", + "title": "Remove Resource Type from List", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.refreshAllResources", + "title": "Refresh All Resources", + "icon": "$(refresh)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.stacks.refresh", + "title": "Refresh Stacks", + "icon": "$(refresh)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.loadMoreStacks", + "title": "Load More Stacks", + "icon": "$(surround-with)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.loadMoreResources", + "title": "Load More Resources", + "icon": "$(surround-with)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.searchResource", + "title": "Find Resource by Identifier", + "icon": "$(search)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.cloneResourceState", + "title": "Clone Resource State", + "icon": "$(copy)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.getStackManagementInfo", + "title": "Get Stack Management Info", + "icon": "$(info)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.server.restartServer", + "title": "Restart Server", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.deployTemplate", + "title": "Validate and Deploy", + "category": "AWS CloudFormation", + "icon": "$(cloud-upload)" + }, + { + "command": "aws.cloudformation.api.deployTemplateFromStacksMenu", + "title": "Validate and Deploy", + "category": "AWS CloudFormation", + "icon": "$(plus)" + }, + { + "command": "aws.cloudformation.api.rerunValidateAndDeploy", + "title": "Rerun Validate and Deploy", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.selectRegion", + "title": "Select Region", + "icon": "$(gear)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.stacks.viewChangeSet", + "title": "View Change Set", + "icon": "$(eye)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.stacks.deleteChangeSet", + "title": "Delete Change Set", + "icon": "$(close)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.stacks.refreshChangeSets", + "title": "Refresh Change Sets", + "icon": "$(refresh)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.stack.view", + "title": "View Stack Detail", + "icon": "$(eye)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.loadMoreChangeSets", + "title": "Load More Change Sets", + "icon": "$(surround-with)", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.init.initializeProject", + "title": "CFN Init: Initialize Project", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.init.addEnvironment", + "title": "CFN Init: Add Environment", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.init.removeEnvironment", + "title": "CFN Init: Remove Environment", + "category": "AWS CloudFormation" + }, + { + "command": "aws.cloudformation.api.addRelatedResources", + "title": "Add Related Resources by Type", + "category": "AWS CloudFormation" } ], "jsonValidation": [ @@ -4296,6 +5148,17 @@ } ], "languages": [ + { + "id": "cloudformation", + "extensions": [ + ".template", + ".cfn" + ], + "aliases": [ + "CloudFormation" + ], + "configuration": "./cloudformation-language-config.json" + }, { "id": "asl", "extensions": [ @@ -4357,6 +5220,11 @@ } ], "grammars": [ + { + "language": "cloudformation", + "scopeName": "source.cloudformation", + "path": "./syntaxes/cloudformation.tmLanguage.json" + }, { "language": "asl", "scopeName": "source.asl", @@ -4401,6 +5269,16 @@ "description": "%AWS.toolkit.lambda.walkthrough.description%", "when": "workspacePlatform != webworker", "steps": [ + { + "id": "toolInstallWindows", + "title": "%AWS.toolkit.lambda.walkthrough.toolInstall.title%", + "description": "%AWS.toolkit.lambda.walkthrough.toolInstall.description.windows%", + "media": { + "image": "./resources/walkthrough/appBuilder/install.png", + "altText": "Showing GUI installer" + }, + "when": "isWindows" + }, { "id": "toolInstall", "title": "%AWS.toolkit.lambda.walkthrough.toolInstall.title%", @@ -4408,7 +5286,8 @@ "media": { "image": "./resources/walkthrough/appBuilder/install.png", "altText": "Showing GUI installer" - } + }, + "when": "!isWindows" }, { "id": "chooseTemplate", @@ -4725,124 +5604,173 @@ "fontCharacter": "\\f1d2" } }, - "aws-lambda-function": { + "aws-lambda-deployed-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d3" } }, - "aws-mynah-MynahIconBlack": { + "aws-lambda-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d4" } }, - "aws-mynah-MynahIconWhite": { + "aws-lambda-invoke-remotely": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d5" } }, - "aws-mynah-logo": { + "aws-mynah-MynahIconBlack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d6" } }, - "aws-redshift-cluster": { + "aws-mynah-MynahIconWhite": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d7" } }, - "aws-redshift-cluster-connected": { + "aws-mynah-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d8" } }, - "aws-redshift-database": { + "aws-redshift-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d9" } }, - "aws-redshift-redshift-cluster-connected": { + "aws-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1da" } }, - "aws-redshift-schema": { + "aws-redshift-database": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1db" } }, - "aws-redshift-table": { + "aws-redshift-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1dc" } }, - "aws-s3-bucket": { + "aws-redshift-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1dd" } }, - "aws-s3-create-bucket": { + "aws-redshift-table": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1de" } }, - "aws-sagemaker-code-editor": { + "aws-s3-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1df" } }, - "aws-sagemaker-jupyter-lab": { + "aws-s3-create-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e0" } }, - "aws-schemas-registry": { + "aws-sagemaker-code-editor": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e1" } }, - "aws-schemas-schema": { + "aws-sagemaker-jupyter-lab": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e2" } }, - "aws-stepfunctions-preview": { + "aws-sagemakerunifiedstudio-catalog": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e3" } + }, + "aws-sagemakerunifiedstudio-spaces": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e4" + } + }, + "aws-sagemakerunifiedstudio-spaces-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e5" + } + }, + "aws-sagemakerunifiedstudio-symbol-int": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e6" + } + }, + "aws-sagemakerunifiedstudio-table": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e7" + } + }, + "aws-schemas-registry": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e8" + } + }, + "aws-schemas-schema": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e9" + } + }, + "aws-stepfunctions-preview": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1ea" + } } }, "notebooks": [ @@ -4887,6 +5815,14 @@ "{git,gitlens,conflictResolution,vscode-local-history}:/**/*.tc.json": "default", "{git,gitlens,conflictResolution,vscode-local-history}:/**/*.{asl.json,asl.yaml,asl.yml}": "default" } + }, + "[cloudformation]": { + "editor.quickSuggestions": { + "other": "on", + "comments": "off", + "strings": "on" + }, + "editor.suggest.showWords": false } }, "devDependencies": {}, diff --git a/packages/toolkit/syntaxes/cloudformation.tmLanguage.json b/packages/toolkit/syntaxes/cloudformation.tmLanguage.json new file mode 100644 index 00000000000..d66db236cc0 --- /dev/null +++ b/packages/toolkit/syntaxes/cloudformation.tmLanguage.json @@ -0,0 +1,868 @@ +{ + "version": "1.0.0", + "name": "CloudFormation", + "scopeName": "source.cloudformation", + "fileTypes": ["cfn", "template"], + "patterns": [ + { + "begin": "^\\s*\\{", + "end": "\\z", + "name": "meta.cloudformation.json", + "patterns": [ + { + "include": "source.json" + } + ] + }, + { + "begin": "^(?!\\s*\\{)", + "end": "\\z", + "name": "meta.cloudformation.yaml", + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#cfn-logical-ids" + }, + { + "include": "#cfn-top-level-keys" + }, + { + "include": "#property" + }, + { + "include": "#directive" + }, + { + "match": "^---", + "name": "entity.other.document.begin.yaml" + }, + { + "match": "^\\.{3}", + "name": "entity.other.document.end.yaml" + }, + { + "include": "#node" + } + ] + } + ], + "repository": { + "cfn-top-level-keys": { + "patterns": [ + { + "match": "^(AWSTemplateFormatVersion|Description|Metadata|Parameters|Mappings|Conditions|Transform|Resources|Outputs)\\s*:", + "captures": { + "1": { + "name": "entity.name.tag.cloudformation.top-level" + } + } + } + ] + }, + "cfn-logical-ids": { + "patterns": [ + { + "begin": "^(Resources)\\s*:", + "beginCaptures": { + "1": { + "name": "entity.name.tag.cloudformation.top-level" + } + }, + "end": "^(?=\\S)", + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "^(\\s+)([A-Za-z][A-Za-z0-9]*)\\s*:", + "beginCaptures": { + "2": { + "name": "entity.name.function.cloudformation.resource-id" + } + }, + "end": "^(?!\\1\\s|\\1$)", + "patterns": [ + { + "include": "#node" + } + ] + } + ] + }, + { + "begin": "^(Parameters)\\s*:", + "beginCaptures": { + "1": { + "name": "entity.name.tag.cloudformation.top-level" + } + }, + "end": "^(?=\\S)", + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "^(\\s+)([A-Za-z][A-Za-z0-9]*)\\s*:", + "beginCaptures": { + "2": { + "name": "entity.name.function.cloudformation.parameter-id" + } + }, + "end": "^(?!\\1\\s|\\1$)", + "patterns": [ + { + "include": "#node" + } + ] + } + ] + }, + { + "begin": "^(Conditions)\\s*:", + "beginCaptures": { + "1": { + "name": "entity.name.tag.cloudformation.top-level" + } + }, + "end": "^(?=\\S)", + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "^(\\s+)([A-Za-z][A-Za-z0-9]*)\\s*:", + "beginCaptures": { + "2": { + "name": "entity.name.function.cloudformation.condition-id" + } + }, + "end": "^(?!\\1\\s|\\1$)", + "patterns": [ + { + "include": "#node" + } + ] + } + ] + }, + { + "begin": "^(Outputs)\\s*:", + "beginCaptures": { + "1": { + "name": "entity.name.tag.cloudformation.top-level" + } + }, + "end": "^(?=\\S)", + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "^(\\s+)([A-Za-z][A-Za-z0-9]*)\\s*:", + "beginCaptures": { + "2": { + "name": "entity.name.function.cloudformation.output-id" + } + }, + "end": "^(?!\\1\\s|\\1$)", + "patterns": [ + { + "include": "#node" + } + ] + } + ] + }, + { + "begin": "^(Mappings)\\s*:", + "beginCaptures": { + "1": { + "name": "entity.name.tag.cloudformation.top-level" + } + }, + "end": "^(?=\\S)", + "patterns": [ + { + "include": "#comment" + }, + { + "begin": "^(\\s+)([A-Za-z][A-Za-z0-9]*)\\s*:", + "beginCaptures": { + "2": { + "name": "entity.name.function.cloudformation.mapping-id" + } + }, + "end": "^(?!\\1\\s|\\1$)", + "patterns": [ + { + "include": "#node" + } + ] + } + ] + } + ] + }, + "cfn-functions": { + "patterns": [ + { + "match": "!(Ref|GetAtt|GetAZs|ImportValue|Join|Split|Select|Sub|Base64|GetParam|Equals|If|Not|And|Or|FindInMap|Condition)\\b", + "name": "keyword.control.cloudformation.function" + }, + { + "match": "Fn::(GetAtt|GetAZs|ImportValue|Join|Split|Select|Sub|Base64|GetParam|Equals|If|Not|And|Or|FindInMap)", + "name": "keyword.control.cloudformation.function" + }, + { + "match": "\\bRef(?=\\s*:)", + "name": "keyword.control.cloudformation.function" + } + ] + }, + "cfn-sub-parameters": { + "patterns": [ + { + "match": "\\$\\{(AWS::(AccountId|NotificationARNs|NoValue|Partition|Region|StackId|StackName|URLSuffix))\\}", + "name": "variable.language.cloudformation.pseudo-parameter" + }, + { + "match": "\\$\\{[^}]+\\}", + "name": "variable.other.cloudformation.sub-parameter" + } + ] + }, + "block-collection": { + "patterns": [ + { + "include": "#block-sequence" + }, + { + "include": "#block-mapping" + } + ] + }, + "block-mapping": { + "patterns": [ + { + "include": "#block-pair" + } + ] + }, + "block-node": { + "patterns": [ + { + "include": "#prototype" + }, + { + "include": "#block-scalar" + }, + { + "include": "#block-collection" + }, + { + "include": "#flow-scalar-plain-out" + }, + { + "include": "#flow-node" + } + ] + }, + "block-pair": { + "patterns": [ + { + "begin": "\\?", + "beginCaptures": { + "1": { + "name": "punctuation.definition.key-value.begin.yaml" + } + }, + "end": "(?=\\?)|^ *(:)|(:)", + "endCaptures": { + "1": { + "name": "punctuation.separator.key-value.mapping.yaml" + }, + "2": { + "name": "invalid.illegal.expected-newline.yaml" + } + }, + "name": "meta.block-mapping.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + { + "begin": "(?x)\n (?=\n (?x:\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n )\n (\n [^\\s:]\n | : \\S\n | \\s+ (?![#\\s])\n )*\n \\s*\n :\n\t\t\t\t\t\t\t(\\s|$)\n )\n ", + "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", + "name": "meta.map.key.yaml", + "patterns": [ + { + "include": "#flow-scalar-plain-out-implicit-type" + }, + { + "include": "#cfn-functions" + }, + { + "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n ", + "beginCaptures": { + "0": { + "name": "entity.name.tag.yaml" + } + }, + "contentName": "entity.name.tag.yaml", + "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", + "name": "string.unquoted.plain.out.yaml" + } + ] + }, + { + "match": ":(?=\\s|$)", + "name": "punctuation.separator.key-value.mapping.yaml" + } + ] + }, + "block-scalar": { + "begin": "(?:(\\|)|(>))([1-9])?([-+])?(.*\\n?)", + "beginCaptures": { + "1": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "3": { + "name": "constant.numeric.indentation-indicator.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + }, + "5": { + "patterns": [ + { + "include": "#comment" + }, + { + "match": ".+", + "name": "invalid.illegal.expected-comment-or-newline.yaml" + } + ] + } + }, + "end": "^(?=\\S)|(?!\\G)", + "patterns": [ + { + "begin": "^([ ]+)(?! )", + "end": "^(?!\\1|\\s*$)", + "name": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#cfn-sub-parameters" + } + ] + } + ] + }, + "block-sequence": { + "match": "(-)(?!\\S)", + "name": "punctuation.definition.block.sequence.item.yaml" + }, + "comment": { + "begin": "(?:(^[ \\t]*)|[ \\t]+)(?=#\\p{Print}*$)", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.comment.leading.yaml" + } + }, + "end": "(?!\\G)", + "patterns": [ + { + "begin": "#", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.yaml" + } + }, + "end": "\\n", + "name": "comment.line.number-sign.yaml" + } + ] + }, + "directive": { + "begin": "^%", + "beginCaptures": { + "0": { + "name": "punctuation.definition.directive.begin.yaml" + } + }, + "end": "(?=$|[ \\t]+($|#))", + "name": "meta.directive.yaml", + "patterns": [ + { + "captures": { + "1": { + "name": "keyword.other.directive.yaml.yaml" + }, + "2": { + "name": "constant.numeric.yaml-version.yaml" + } + }, + "match": "\\G(YAML)[ \\t]+(\\d+\\.\\d+)" + }, + { + "captures": { + "1": { + "name": "keyword.other.directive.tag.yaml" + }, + "2": { + "name": "storage.type.tag-handle.yaml" + }, + "3": { + "name": "support.type.tag-prefix.yaml" + } + }, + "match": "(?x)\n \\G\n (TAG)\n (?:[ \\t]+\n ((?:!(?:[0-9A-Za-z\\-]*!)?))\n (?:[ \\t]+ (\n ! (?x: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )*\n | (?![,!\\[\\]{}]) (?x: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )+\n )\n )?\n )?\n " + }, + { + "captures": { + "1": { + "name": "support.other.directive.reserved.yaml" + }, + "2": { + "name": "string.unquoted.directive-name.yaml" + }, + "3": { + "name": "string.unquoted.directive-parameter.yaml" + } + }, + "match": "(?x) \\G (\\w+) (?:[ \\t]+ (\\w+) (?:[ \\t]+ (\\w+))? )?" + }, + { + "match": "\\S+", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + }, + "flow-alias": { + "captures": { + "1": { + "name": "keyword.control.flow.alias.yaml" + }, + "2": { + "name": "punctuation.definition.alias.yaml" + }, + "3": { + "name": "variable.other.alias.yaml" + }, + "4": { + "name": "invalid.illegal.character.anchor.yaml" + } + }, + "match": "((\\*))([^\\s\\[\\]/{/},]+)([^\\s\\]},]\\S*)?" + }, + "flow-collection": { + "patterns": [ + { + "include": "#flow-sequence" + }, + { + "include": "#flow-mapping" + } + ] + }, + "flow-mapping": { + "begin": "\\{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.mapping.begin.yaml" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.mapping.end.yaml" + } + }, + "name": "meta.flow-mapping.yaml", + "patterns": [ + { + "include": "#prototype" + }, + { + "match": ",", + "name": "punctuation.separator.mapping.yaml" + }, + { + "include": "#flow-pair" + } + ] + }, + "flow-node": { + "patterns": [ + { + "include": "#prototype" + }, + { + "include": "#flow-alias" + }, + { + "include": "#flow-collection" + }, + { + "include": "#flow-scalar" + } + ] + }, + "flow-pair": { + "patterns": [ + { + "match": "\"((?:\\\\.|[^\"])*)\"\\s*(?=:)", + "captures": { + "0": { + "name": "entity.name.tag.yaml" + } + } + }, + { + "match": "'((?:''|[^'])*)'\\s*(?=:)", + "captures": { + "0": { + "name": "entity.name.tag.yaml" + } + } + }, + { + "begin": "\\?", + "beginCaptures": { + "0": { + "name": "punctuation.definition.key-value.begin.yaml" + } + }, + "end": "(?=[},\\]])", + "name": "meta.flow-pair.explicit.yaml", + "patterns": [ + { + "include": "#prototype" + }, + { + "include": "#flow-pair" + }, + { + "include": "#flow-node" + }, + { + "begin": ":(?=\\s|$|[\\[\\]{},])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.key-value.mapping.yaml" + } + }, + "end": "(?=[},\\]])", + "patterns": [ + { + "include": "#flow-value" + } + ] + } + ] + }, + { + "begin": "(?x)\n (?=\n (?:\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n )\n (\n [^\\s:[\\[\\]{},]]\n | : [^\\s[\\[\\]{},]]\n | \\s+ (?![#\\s])\n )*\n \\s*\n :\n\t\t\t\t\t\t\t(\\s|$)\n )\n ", + "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#flow-scalar-plain-in-implicit-type" + }, + { + "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n ", + "beginCaptures": { + "0": { + "name": "entity.name.tag.yaml" + } + }, + "contentName": "entity.name.tag.yaml", + "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", + "name": "string.unquoted.plain.in.yaml" + } + ] + }, + { + "include": "#flow-node" + }, + { + "begin": ":(?=\\s|$|[\\[\\]{},])", + "captures": { + "0": { + "name": "punctuation.separator.key-value.mapping.yaml" + } + }, + "end": "(?=[},\\]])", + "name": "meta.flow-pair.yaml", + "patterns": [ + { + "include": "#flow-value" + } + ] + } + ] + }, + "flow-scalar": { + "patterns": [ + { + "include": "#flow-scalar-double-quoted" + }, + { + "include": "#flow-scalar-single-quoted" + }, + { + "include": "#flow-scalar-plain-in" + } + ] + }, + "flow-scalar-double-quoted": { + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "string.quoted.double.yaml", + "patterns": [ + { + "include": "#cfn-sub-parameters" + }, + { + "match": "\\\\([0abtnvfre \"/\\\\N_Lp]|x\\d\\d|u\\d{4}|U\\d{8})", + "name": "constant.character.escape.yaml" + }, + { + "match": "\\\\\\n", + "name": "constant.character.escape.double-quoted.newline.yaml" + } + ] + }, + "flow-scalar-plain-in": { + "patterns": [ + { + "include": "#flow-scalar-plain-in-implicit-type" + }, + { + "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n ", + "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", + "name": "string.unquoted.plain.in.yaml", + "patterns": [ + { + "include": "#cfn-sub-parameters" + } + ] + } + ] + }, + "flow-scalar-plain-in-implicit-type": { + "patterns": [ + { + "captures": { + "1": { + "name": "constant.language.null.yaml" + }, + "2": { + "name": "constant.language.boolean.yaml" + }, + "3": { + "name": "constant.numeric.integer.yaml" + }, + "4": { + "name": "constant.numeric.float.yaml" + }, + "5": { + "name": "constant.other.timestamp.yaml" + }, + "6": { + "name": "constant.language.value.yaml" + }, + "7": { + "name": "constant.language.merge.yaml" + } + }, + "match": "(?x)\n (?x:\n (null|Null|NULL|~)\n | (y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)\n | (\n (?:\n [-+]? 0b [0-1_]+ # (base 2)\n | [-+]? 0 [0-7_]+ # (base 8)\n | [-+]? (?: 0|[1-9][0-9_]*) # (base 10)\n | [-+]? 0x [0-9a-fA-F_]+ # (base 16)\n | [-+]? [1-9] [0-9_]* (?: :[0-5]?[0-9])+ # (base 60)\n )\n )\n | (\n (?x:\n [-+]? (?: [0-9] [0-9_]*)? \\. [0-9.]* (?: [eE] [-+] [0-9]+)? # (base 10)\n | [-+]? [0-9] [0-9_]* (?: :[0-5]?[0-9])+ \\. [0-9_]* # (base 60)\n | [-+]? \\. (?: inf|Inf|INF) # (infinity)\n | \\. (?: nan|NaN|NAN) # (not a number)\n )\n )\n | (\n (?x:\n \\d{4} - \\d{2} - \\d{2} # (y-m-d)\n | \\d{4} # (year)\n - \\d{1,2} # (month)\n - \\d{1,2} # (day)\n (?: [Tt] | [ \\t]+) \\d{1,2} # (hour)\n : \\d{2} # (minute)\n : \\d{2} # (second)\n (?: \\.\\d*)? # (fraction)\n (?:\n (?:[ \\t]*) Z\n | [-+] \\d{1,2} (?: :\\d{1,2})?\n )? # (time zone)\n )\n )\n | (=)\n | (<<)\n )\n (?:\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n )\n " + } + ] + }, + "flow-scalar-plain-out": { + "patterns": [ + { + "include": "#flow-scalar-plain-out-implicit-type" + }, + { + "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n ", + "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", + "name": "string.unquoted.plain.out.yaml", + "patterns": [ + { + "include": "#cfn-sub-parameters" + } + ] + } + ] + }, + "flow-scalar-plain-out-implicit-type": { + "patterns": [ + { + "captures": { + "1": { + "name": "constant.language.null.yaml" + }, + "2": { + "name": "constant.language.boolean.yaml" + }, + "3": { + "name": "constant.numeric.integer.yaml" + }, + "4": { + "name": "constant.numeric.float.yaml" + }, + "5": { + "name": "constant.other.timestamp.yaml" + }, + "6": { + "name": "constant.language.value.yaml" + }, + "7": { + "name": "constant.language.merge.yaml" + } + }, + "match": "(?x)\n (?x:\n (null|Null|NULL|~)\n | (y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)\n | (\n (?:\n [-+]? 0b [0-1_]+ # (base 2)\n | [-+]? 0 [0-7_]+ # (base 8)\n | [-+]? (?: 0|[1-9][0-9_]*) # (base 10)\n | [-+]? 0x [0-9a-fA-F_]+ # (base 16)\n | [-+]? [1-9] [0-9_]* (?: :[0-5]?[0-9])+ # (base 60)\n )\n )\n | (\n (?x:\n [-+]? (?: [0-9] [0-9_]*)? \\. [0-9.]* (?: [eE] [-+] [0-9]+)? # (base 10)\n | [-+]? [0-9] [0-9_]* (?: :[0-5]?[0-9])+ \\. [0-9_]* # (base 60)\n | [-+]? \\. (?: inf|Inf|INF) # (infinity)\n | \\. (?: nan|NaN|NAN) # (not a number)\n )\n )\n | (\n (?x:\n \\d{4} - \\d{2} - \\d{2} # (y-m-d)\n | \\d{4} # (year)\n - \\d{1,2} # (month)\n - \\d{1,2} # (day)\n (?: [Tt] | [ \\t]+) \\d{1,2} # (hour)\n : \\d{2} # (minute)\n : \\d{2} # (second)\n (?: \\.\\d*)? # (fraction)\n (?:\n (?:[ \\t]*) Z\n | [-+] \\d{1,2} (?: :\\d{1,2})?\n )? # (time zone)\n )\n )\n | (=)\n | (<<)\n )\n (?x:\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n )\n " + } + ] + }, + "flow-scalar-single-quoted": { + "begin": "'", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "end": "'(?!')", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "string.quoted.single.yaml", + "patterns": [ + { + "include": "#cfn-sub-parameters" + }, + { + "match": "''", + "name": "constant.character.escape.single-quoted.yaml" + } + ] + }, + "flow-sequence": { + "begin": "\\[", + "beginCaptures": { + "0": { + "name": "punctuation.definition.sequence.begin.yaml" + } + }, + "end": "\\]", + "endCaptures": { + "0": { + "name": "punctuation.definition.sequence.end.yaml" + } + }, + "name": "meta.flow-sequence.yaml", + "patterns": [ + { + "include": "#prototype" + }, + { + "match": ",", + "name": "punctuation.separator.sequence.yaml" + }, + { + "include": "#flow-pair" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-value": { + "patterns": [ + { + "begin": "\\G(?![},\\]])", + "end": "(?=[},\\]])", + "name": "meta.flow-pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + } + ] + }, + "node": { + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "property": { + "begin": "(?=!|&)", + "end": "(?!\\G)", + "name": "meta.property.yaml", + "patterns": [ + { + "captures": { + "1": { + "name": "keyword.control.property.anchor.yaml" + }, + "2": { + "name": "punctuation.definition.anchor.yaml" + }, + "3": { + "name": "entity.name.type.anchor.yaml" + }, + "4": { + "name": "invalid.illegal.character.anchor.yaml" + } + }, + "match": "\\G((&))([^\\s\\[\\]/{/},]+)(\\S+)?" + }, + { + "include": "#cfn-functions" + }, + { + "match": "(?x)\n \\G\n (?:\n ! < (?: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )+ >\n | (?:!(?:[0-9A-Za-z\\-]*!)?) (?: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$_.~*'()] )+\n | !\n )\n (?=\\ |\\t|$)\n ", + "name": "storage.type.tag-handle.yaml" + }, + { + "match": "\\S+", + "name": "invalid.illegal.tag-handle.yaml" + } + ] + }, + "prototype": { + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#property" + } + ] + } + } +} diff --git a/packages/toolkit/test/e2e/cloudformation/index.ts b/packages/toolkit/test/e2e/cloudformation/index.ts new file mode 100644 index 00000000000..8431e84ed08 --- /dev/null +++ b/packages/toolkit/test/e2e/cloudformation/index.ts @@ -0,0 +1,15 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { runTests } from 'aws-core-vscode/test' +import { VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils' + +export function run(): Promise { + return runTests( + process.env.TEST_DIR ?? ['../../core/dist/src/testE2E/cloudformation'], + VSCODE_EXTENSION_ID.awstoolkit, + ['../../core/dist/src/testInteg/globalSetup.test.ts'] + ) +} diff --git a/plugins/eslint-plugin-aws-toolkits/package.json b/plugins/eslint-plugin-aws-toolkits/package.json index b10e57b1c38..924b08e2b95 100644 --- a/plugins/eslint-plugin-aws-toolkits/package.json +++ b/plugins/eslint-plugin-aws-toolkits/package.json @@ -9,6 +9,7 @@ "clean": "ts-node ../../scripts/clean.ts dist" }, "devDependencies": { + "@types/eslint": "^8.56.0", "mocha": "^10.1.0" }, "engines": { diff --git a/scripts/scan-licenses.sh b/scripts/scan-licenses.sh new file mode 100644 index 00000000000..25ba2781356 --- /dev/null +++ b/scripts/scan-licenses.sh @@ -0,0 +1,83 @@ +#!/bin/bash +banner() +{ + echo "*****************************************" + echo "** AWS Toolkit License Scanner **" + echo "*****************************************" + echo "" +} + +help() +{ + banner + echo "Usage: ./scan-licenses.sh" + echo "" + echo "This script scans the npm dependencies in the current project" + echo "and generates license reports and attribution documents." + echo "" +} + +gen_attribution(){ + echo "" + echo " == Generating Attribution Document ==" + npm install -g oss-attribution-generator + generate-attribution + if [ -d "oss-attribution" ]; then + mv oss-attribution/attribution.txt LICENSE-THIRD-PARTY + rm -rf oss-attribution + echo "Attribution document generated: LICENSE-THIRD-PARTY" + else + echo "Warning: oss-attribution directory not found" + fi +} + +gen_full_license_report(){ + echo "" + echo " == Generating Full License Report ==" + npm install -g license-checker + license-checker --json > licenses-full.json + echo "Full license report generated: licenses-full.json" +} + +main() +{ + banner + + # Check if we're in the right directory + if [ ! -f "package.json" ]; then + echo "Error: package.json not found. Please run this script from the project root." + exit 1 + fi + + # Check if node_modules exists + if [ ! -d "node_modules" ]; then + echo "node_modules not found. Running npm install..." + npm install + if [ $? -ne 0 ]; then + echo "Error: npm install failed" + exit 1 + fi + fi + + echo "Scanning licenses for AWS Toolkit VS Code project..." + echo "Project root: $(pwd)" + echo "" + + gen_attribution + gen_full_license_report + + echo "" + echo "=== License Scan Complete ===" + echo "Generated files:" + echo " - LICENSE-THIRD-PARTY (attribution document)" + echo " - licenses-full.json (complete license data)" + echo "" +} + +if [ "$1" = "--help" ] || [ "$1" = "-h" ] +then + help + exit 0 +else + main +fi \ No newline at end of file diff --git a/scripts/scan-licenses.ts b/scripts/scan-licenses.ts new file mode 100644 index 00000000000..75759d930d0 --- /dev/null +++ b/scripts/scan-licenses.ts @@ -0,0 +1,83 @@ +#!/usr/bin/env node + +import { execSync } from 'child_process' +import { existsSync, rmSync, renameSync, writeFileSync } from 'fs' +import { join } from 'path' + +function banner() { + console.log('*****************************************') + console.log('** AWS Toolkit License Scanner **') + console.log('*****************************************') + console.log('') +} + +function genAttribution() { + console.log('') + console.log(' == Generating Attribution Document ==') + + try { + execSync('npm install -g oss-attribution-generator', { stdio: 'inherit' }) + execSync('generate-attribution', { stdio: 'inherit' }) + + if (existsSync('oss-attribution')) { + renameSync(join('oss-attribution', 'attribution.txt'), 'LICENSE-THIRD-PARTY') + rmSync('oss-attribution', { recursive: true, force: true }) + console.log('Attribution document generated: LICENSE-THIRD-PARTY') + } else { + console.log('Warning: oss-attribution directory not found') + } + } catch (error) { + console.error('Error generating attribution:', error) + } +} + +function genFullLicenseReport() { + console.log('') + console.log(' == Generating Full License Report ==') + + try { + execSync('npm install -g license-checker', { stdio: 'inherit' }) + const licenseData = execSync('license-checker --json', { encoding: 'utf8' }) + writeFileSync('licenses-full.json', licenseData) + console.log('Full license report generated: licenses-full.json') + } catch (error) { + console.error('Error generating license report:', error) + } +} + +function main() { + banner() + + if (!existsSync('package.json')) { + console.error('Error: package.json not found. Please run this script from the project root.') + process.exit(1) + } + + if (!existsSync('node_modules')) { + console.log('node_modules not found. Running npm install...') + try { + execSync('npm install', { stdio: 'inherit' }) + } catch (error) { + console.error('Error running npm install:', error) + process.exit(1) + } + } + + console.log('Scanning licenses for AWS Toolkit VS Code project...') + console.log(`Project root: ${process.cwd()}`) + console.log('') + + genAttribution() + genFullLicenseReport() + + console.log('') + console.log('=== License Scan Complete ===') + console.log('Generated files:') + console.log(' - LICENSE-THIRD-PARTY (attribution document)') + console.log(' - licenses-full.json (complete license data)') + console.log('') +} + +if (require.main === module) { + main() +} diff --git a/src.gen/@amzn/glue-catalog-client/0.0.1.tgz b/src.gen/@amzn/glue-catalog-client/0.0.1.tgz new file mode 100644 index 00000000000..e8c536d3ba0 Binary files /dev/null and b/src.gen/@amzn/glue-catalog-client/0.0.1.tgz differ